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

realm / realm-core / thomas.goyne_484

05 Aug 2024 04:20PM UTC coverage: 91.097% (-0.01%) from 91.108%
thomas.goyne_484

Pull #7912

Evergreen

tgoyne
Extract some duplicated code for sync triggers and timers
Pull Request #7912: Extract some duplicated code for sync triggers and timers

102644 of 181486 branches covered (56.56%)

62 of 71 new or added lines in 6 files covered. (87.32%)

87 existing lines in 14 files now uncovered.

216695 of 237872 relevant lines covered (91.1%)

5798402.26 hits per line

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

98.35
/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 <filesystem>
57
#include <iostream>
58
#include <stdexcept>
59

60
using namespace std::string_literals;
61

62
namespace realm {
63

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

72
} // namespace realm
73

74
namespace realm::app {
75

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

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

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

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

127
            ret.push_back(id);
80✔
128
        }
80✔
129
    });
16✔
130
    return ret;
16✔
131
}
16✔
132

133
} // namespace
134

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

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

154

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

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

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

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

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

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

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

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

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

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

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

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

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

268
TEST_CASE("app: error handling integration test", "[sync][flx][baas]") {
18✔
269
    static std::optional<FLXSyncTestHarness> harness{"error_handling"};
18✔
270
    create_user_and_log_in(harness->app());
18✔
271
    SyncTestFile config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
18✔
272
    auto&& [error_future, error_handler] = make_error_handler();
18✔
273
    config.sync_config->error_handler = std::move(error_handler);
18✔
274
    config.sync_config->client_resync_mode = ClientResyncMode::Manual;
18✔
275

276
    SECTION("Resuming while waiting for session to auto-resume") {
18✔
277
        enum class TestState { InitialSuspend, InitialResume, SecondBind, SecondSuspend, SecondResume, Done };
2✔
278
        TestingStateMachine<TestState> state(TestState::InitialSuspend);
2✔
279
        config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>,
2✔
280
                                                            const SyncClientHookData& data) {
50✔
281
            std::optional<TestState> wait_for;
50✔
282
            auto event = data.event;
50✔
283
            state.transition_with([&](TestState state) -> std::optional<TestState> {
50✔
284
                if (state == TestState::InitialSuspend && event == SyncClientHookEvent::SessionSuspended) {
50✔
285
                    // If we're getting suspended for the first time, notify the test thread that we're
286
                    // ready to be resumed.
287
                    wait_for = TestState::SecondBind;
2✔
288
                    return TestState::InitialResume;
2✔
289
                }
2✔
290
                else if (state == TestState::SecondBind && data.event == SyncClientHookEvent::BindMessageSent) {
48✔
291
                    return TestState::SecondSuspend;
2✔
292
                }
2✔
293
                else if (state == TestState::SecondSuspend && event == SyncClientHookEvent::SessionSuspended) {
46✔
294
                    wait_for = TestState::Done;
2✔
295
                    return TestState::SecondResume;
2✔
296
                }
2✔
297
                return std::nullopt;
44✔
298
            });
50✔
299
            if (wait_for) {
50✔
300
                state.wait_for(*wait_for);
4✔
301
            }
4✔
302
            return SyncClientHookAction::NoAction;
50✔
303
        };
50✔
304
        auto r = Realm::get_shared_realm(config);
2✔
305
        wait_for_upload(*r);
2✔
306
        nlohmann::json error_body = {
2✔
307
            {"tryAgain", true},           {"message", "fake error"},
2✔
308
            {"shouldClientReset", false}, {"isRecoveryModeDisabled", false},
2✔
309
            {"action", "Transient"},      {"backoffIntervalSec", 900},
2✔
310
            {"backoffMaxDelaySec", 900},  {"backoffMultiplier", 1},
2✔
311
        };
2✔
312
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
313
                                       {"args", nlohmann::json{{"errorCode", 229}, {"errorBody", error_body}}}};
2✔
314

315
        // First we trigger a retryable transient error that should cause the client to try to resume the
316
        // session in 5 minutes.
317
        auto test_cmd_res =
2✔
318
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
319
                .get();
2✔
320
        REQUIRE(test_cmd_res == "{}");
2!
321

322
        // Wait for the
323
        state.wait_for(TestState::InitialResume);
2✔
324

325
        // Once we're suspended, immediately tell the sync client to resume the session. This should cancel the
326
        // timer that would have auto-resumed the session.
327
        r->sync_session()->handle_reconnect();
2✔
328
        state.transition_with([&](TestState cur_state) {
2✔
329
            REQUIRE(cur_state == TestState::InitialResume);
2!
330
            return TestState::SecondBind;
2✔
331
        });
2✔
332
        state.wait_for(TestState::SecondSuspend);
2✔
333

334
        // Once we're connected again trigger another retryable transient error. Before RCORE-1770 the timer
335
        // to auto-resume the session would have still been active here and we would crash when trying to start
336
        // a second timer to auto-resume after this error.
337
        test_cmd_res =
2✔
338
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
339
                .get();
2✔
340
        REQUIRE(test_cmd_res == "{}");
2!
341
        state.wait_for(TestState::SecondResume);
2✔
342

343
        // Finally resume the session again which should cancel the second timer and the session should auto-resume
344
        // normally without crashing.
345
        r->sync_session()->handle_reconnect();
2✔
346
        state.transition_with([&](TestState cur_state) {
2✔
347
            REQUIRE(cur_state == TestState::SecondResume);
2!
348
            return TestState::Done;
2✔
349
        });
2✔
350
        wait_for_download(*r);
2✔
351
    }
2✔
352

353
    SECTION("handles unknown errors gracefully") {
18✔
354
        auto r = Realm::get_shared_realm(config);
2✔
355
        wait_for_download(*r);
2✔
356
        nlohmann::json error_body = {
2✔
357
            {"tryAgain", false},         {"message", "fake error"},
2✔
358
            {"shouldClientReset", true}, {"isRecoveryModeDisabled", false},
2✔
359
            {"action", "ClientReset"},
2✔
360
        };
2✔
361
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
362
                                       {"args", nlohmann::json{{"errorCode", 299}, {"errorBody", error_body}}}};
2✔
363
        auto test_cmd_res =
2✔
364
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
365
                .get();
2✔
366
        REQUIRE(test_cmd_res == "{}");
2!
367
        auto error = wait_for_future(std::move(error_future)).get();
2✔
368
        REQUIRE(error.status == ErrorCodes::UnknownError);
2!
369
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
2!
370
        REQUIRE(error.is_fatal);
2!
371
        REQUIRE_THAT(error.status.reason(),
2✔
372
                     Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299"));
2✔
373
        REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error"));
2✔
374
    }
2✔
375

376
    SECTION("unknown errors without actions are application bugs") {
18✔
377
        auto r = Realm::get_shared_realm(config);
2✔
378
        wait_for_download(*r);
2✔
379
        nlohmann::json error_body = {
2✔
380
            {"tryAgain", false},
2✔
381
            {"message", "fake error"},
2✔
382
            {"shouldClientReset", false},
2✔
383
            {"isRecoveryModeDisabled", false},
2✔
384
        };
2✔
385
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
386
                                       {"args", nlohmann::json{{"errorCode", 299}, {"errorBody", error_body}}}};
2✔
387
        auto test_cmd_res =
2✔
388
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
389
                .get();
2✔
390
        REQUIRE(test_cmd_res == "{}");
2!
391
        auto error = wait_for_future(std::move(error_future)).get();
2✔
392
        REQUIRE(error.status == ErrorCodes::UnknownError);
2!
393
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
394
        REQUIRE(error.is_fatal);
2!
395
        REQUIRE_THAT(error.status.reason(),
2✔
396
                     Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299"));
2✔
397
        REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error"));
2✔
398
    }
2✔
399

400
    SECTION("handles unknown actions gracefully") {
18✔
401
        auto r = Realm::get_shared_realm(config);
2✔
402
        wait_for_download(*r);
2✔
403
        nlohmann::json error_body = {
2✔
404
            {"tryAgain", false},
2✔
405
            {"message", "fake error"},
2✔
406
            {"shouldClientReset", true},
2✔
407
            {"isRecoveryModeDisabled", false},
2✔
408
            {"action", "FakeActionThatWillNeverExist"},
2✔
409
        };
2✔
410
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
411
                                       {"args", nlohmann::json{{"errorCode", 201}, {"errorBody", error_body}}}};
2✔
412
        auto test_cmd_res =
2✔
413
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
414
                .get();
2✔
415
        REQUIRE(test_cmd_res == "{}");
2!
416
        auto error = wait_for_future(std::move(error_future)).get();
2✔
417
        REQUIRE(error.status == ErrorCodes::RuntimeError);
2!
418
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
419
        REQUIRE(error.is_fatal);
2!
420
        REQUIRE_THAT(error.status.reason(), !Catch::Matchers::ContainsSubstring("Unknown sync protocol error code"));
2✔
421
        REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error"));
2✔
422
    }
2✔
423

424

425
    SECTION("unknown connection-level errors are still errors") {
18✔
426
        auto r = Realm::get_shared_realm(config);
2✔
427
        wait_for_download(*r);
2✔
428
        nlohmann::json error_body = {{"tryAgain", false},
2✔
429
                                     {"message", "fake error"},
2✔
430
                                     {"shouldClientReset", false},
2✔
431
                                     {"isRecoveryModeDisabled", false},
2✔
432
                                     {"action", "ApplicationBug"}};
2✔
433
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
434
                                       {"args", nlohmann::json{{"errorCode", 199}, {"errorBody", error_body}}}};
2✔
435
        auto test_cmd_res =
2✔
436
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
437
                .get();
2✔
438
        REQUIRE(test_cmd_res == "{}");
2!
439
        auto error = wait_for_future(std::move(error_future)).get();
2✔
440
        REQUIRE(error.status == ErrorCodes::SyncProtocolInvariantFailed);
2!
441
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ProtocolViolation);
2!
442
        REQUIRE(error.is_fatal);
2!
443
    }
2✔
444

445
    SECTION("client reset errors") {
18✔
446
        auto r = Realm::get_shared_realm(config);
6✔
447
        wait_for_download(*r);
6✔
448
        nlohmann::json error_body = {{"tryAgain", false},
6✔
449
                                     {"message", "fake error"},
6✔
450
                                     {"shouldClientReset", true},
6✔
451
                                     {"isRecoveryModeDisabled", false},
6✔
452
                                     {"action", "ClientReset"}};
6✔
453
        auto code = GENERATE(sync::ProtocolError::bad_client_file_ident, sync::ProtocolError::bad_server_version,
6✔
454
                             sync::ProtocolError::diverging_histories);
6✔
455
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
6✔
456
                                       {"args", nlohmann::json{{"errorCode", code}, {"errorBody", error_body}}}};
6✔
457
        auto test_cmd_res =
6✔
458
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
6✔
459
                .get();
6✔
460
        REQUIRE(test_cmd_res == "{}");
6!
461
        auto error = wait_for_future(std::move(error_future)).get();
6✔
462
        REQUIRE(error.status == ErrorCodes::SyncClientResetRequired);
6!
463
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
6!
464
        REQUIRE(error.is_client_reset_requested());
6!
465
        REQUIRE(error.is_fatal);
6!
466
    }
6✔
467

468

469
    SECTION("teardown") {
18✔
470
        harness.reset();
2✔
471
    }
2✔
472
}
18✔
473

474

475
TEST_CASE("flx: client reset", "[sync][flx][client reset][baas]") {
46✔
476
    std::vector<ObjectSchema> schema{
46✔
477
        {"TopLevel",
46✔
478
         {
46✔
479
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
46✔
480
             {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
46✔
481
             {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
46✔
482
             {"non_queryable_field", PropertyType::String | PropertyType::Nullable},
46✔
483
             {"list_of_ints_field", PropertyType::Int | PropertyType::Array},
46✔
484
             {"sum_of_list_field", PropertyType::Int},
46✔
485
             {"any_mixed", PropertyType::Mixed | PropertyType::Nullable},
46✔
486
             {"dictionary_mixed", PropertyType::Dictionary | PropertyType::Mixed | PropertyType::Nullable},
46✔
487
         }},
46✔
488
        {"TopLevel2",
46✔
489
         {
46✔
490
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
46✔
491
             {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
46✔
492
         }},
46✔
493
    };
46✔
494

495
    // some of these tests make additive schema changes which is only allowed in dev mode
496
    constexpr bool dev_mode = true;
46✔
497
    FLXSyncTestHarness harness("flx_client_reset",
46✔
498
                               {schema, {"queryable_str_field", "queryable_int_field"}, {}, dev_mode});
46✔
499

500
    auto add_object = [](SharedRealm realm, std::string str_field, int64_t int_field,
46✔
501
                         ObjectId oid = ObjectId::gen()) {
122✔
502
        CppContext c(realm);
122✔
503
        realm->begin_transaction();
122✔
504

505
        int64_t r1 = random_int();
122✔
506
        int64_t r2 = random_int();
122✔
507
        int64_t r3 = random_int();
122✔
508
        int64_t sum = uint64_t(r1) + r2 + r3;
122✔
509

510
        Object::create(c, realm, "TopLevel",
122✔
511
                       std::any(AnyDict{{"_id", oid},
122✔
512
                                        {"queryable_str_field", str_field},
122✔
513
                                        {"queryable_int_field", int_field},
122✔
514
                                        {"non_queryable_field", "non queryable 1"s},
122✔
515
                                        {"list_of_ints_field", std::vector<std::any>{r1, r2, r3}},
122✔
516
                                        {"sum_of_list_field", sum}}));
122✔
517
        realm->commit_transaction();
122✔
518
    };
122✔
519

520
    auto subscribe_to_and_add_objects = [&](SharedRealm realm, size_t num_objects) {
46✔
521
        auto table = realm->read_group().get_table("class_TopLevel");
40✔
522
        auto id_col = table->get_primary_key_column();
40✔
523
        auto sub_set = realm->get_latest_subscription_set();
40✔
524
        for (size_t i = 0; i < num_objects; ++i) {
130✔
525
            auto oid = ObjectId::gen();
90✔
526
            auto mut_sub = sub_set.make_mutable_copy();
90✔
527
            mut_sub.clear();
90✔
528
            mut_sub.insert_or_assign(Query(table).equal(id_col, oid));
90✔
529
            sub_set = mut_sub.commit();
90✔
530
            add_object(realm, util::format("added _id='%1'", oid), 0, oid);
90✔
531
        }
90✔
532
    };
40✔
533

534
    auto add_subscription_for_new_object = [&](SharedRealm realm, std::string str_field,
46✔
535
                                               int64_t int_field) -> sync::SubscriptionSet {
46✔
536
        auto table = realm->read_group().get_table("class_TopLevel");
28✔
537
        auto queryable_str_field = table->get_column_key("queryable_str_field");
28✔
538
        auto sub_set = realm->get_latest_subscription_set().make_mutable_copy();
28✔
539
        sub_set.insert_or_assign(Query(table).equal(queryable_str_field, StringData(str_field)));
28✔
540
        auto resulting_set = sub_set.commit();
28✔
541
        add_object(realm, str_field, int_field);
28✔
542
        return resulting_set;
28✔
543
    };
28✔
544

545
    auto add_invalid_subscription = [](SharedRealm realm) -> sync::SubscriptionSet {
46✔
546
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
547
        auto queryable_str_field = table->get_column_key("non_queryable_field");
2✔
548
        auto sub_set = realm->get_latest_subscription_set().make_mutable_copy();
2✔
549
        sub_set.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
550
        auto resulting_set = sub_set.commit();
2✔
551
        return resulting_set;
2✔
552
    };
2✔
553

554
    auto count_queries_with_str = [](sync::SubscriptionSet subs, std::string_view str) {
46✔
555
        size_t count = 0;
8✔
556
        for (auto sub : subs) {
14✔
557
            if (sub.query_string.find(str) != std::string::npos) {
14✔
558
                ++count;
6✔
559
            }
6✔
560
        }
14✔
561
        return count;
8✔
562
    };
8✔
563
    create_user_and_log_in(harness.app());
46✔
564
    auto user1 = harness.app()->current_user();
46✔
565
    create_user_and_log_in(harness.app());
46✔
566
    auto user2 = harness.app()->current_user();
46✔
567
    SyncTestFile config_local(user1, harness.schema(), SyncConfig::FLXSyncEnabled{});
46✔
568
    config_local.path += ".local";
46✔
569
    SyncTestFile config_remote(user2, harness.schema(), SyncConfig::FLXSyncEnabled{});
46✔
570
    config_remote.path += ".remote";
46✔
571
    const std::string str_field_value = "foo";
46✔
572
    const int64_t local_added_int = 100;
46✔
573
    const int64_t local_added_int2 = 150;
46✔
574
    const int64_t remote_added_int = 200;
46✔
575
    size_t before_reset_count = 0;
46✔
576
    size_t after_reset_count = 0;
46✔
577
    config_local.sync_config->notify_before_client_reset = [&before_reset_count](SharedRealm) {
46✔
578
        ++before_reset_count;
30✔
579
    };
30✔
580
    config_local.sync_config->notify_after_client_reset = [&after_reset_count](SharedRealm, ThreadSafeReference,
46✔
581
                                                                               bool) {
46✔
582
        ++after_reset_count;
×
583
    };
×
584

585
    config_local.sync_config->on_sync_client_event_hook = [](std::weak_ptr<SyncSession> weak_session,
46✔
586
                                                             const SyncClientHookData& data) {
2,832✔
587
        // To prevent the upload cursors from becoming out of sync when the local realm assumes
588
        // the client file ident of the fresh realm, UPLOAD messages are not allowed during the
589
        // fresh realm download so the server's upload cursor versions start at 0 when the
590
        // local realm resumes after the client reset.
591
        if (data.event == SyncClientHookEvent::UploadMessageSent) {
2,832✔
592
            // If this is an UPLOAD message event, check to see if the fresh realm is being downloaded
593
            if (auto session = weak_session.lock()) {
418✔
594
                // Check for a "fresh" path to determine if this is a client reset fresh download session
595
                if (_impl::client_reset::is_fresh_path(session->path())) {
416✔
596
                    FAIL("UPLOAD messages are not allowed during client reset fresh realm download");
×
597
                }
×
598
            }
416✔
599
        }
418✔
600
        return SyncClientHookAction::NoAction;
2,832✔
601
    };
2,832✔
602

603
    SECTION("Recover: offline writes and subscription (single subscription)") {
46✔
604
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
605
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
606
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
607
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
608
        test_reset
2✔
609
            ->populate_initial_object([&](SharedRealm realm) {
2✔
610
                auto pk_of_added_object = ObjectId::gen();
2✔
611
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
612
                auto table = realm->read_group().get_table(ObjectStore::table_name_for_object_type("TopLevel"));
2✔
613
                REALM_ASSERT(table);
2✔
614
                mut_subs.insert_or_assign(Query(table));
2✔
615
                mut_subs.commit();
2✔
616

617
                realm->begin_transaction();
2✔
618
                CppContext c(realm);
2✔
619
                int64_t r1 = random_int();
2✔
620
                int64_t r2 = random_int();
2✔
621
                int64_t r3 = random_int();
2✔
622
                int64_t sum = uint64_t(r1) + r2 + r3;
2✔
623

624
                Object::create(c, realm, "TopLevel",
2✔
625
                               std::any(AnyDict{{"_id"s, pk_of_added_object},
2✔
626
                                                {"queryable_str_field"s, "initial value"s},
2✔
627
                                                {"list_of_ints_field", std::vector<std::any>{r1, r2, r3}},
2✔
628
                                                {"sum_of_list_field", sum}}));
2✔
629

630
                realm->commit_transaction();
2✔
631
                wait_for_upload(*realm);
2✔
632
                return pk_of_added_object;
2✔
633
            })
2✔
634
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
635
                add_object(local_realm, str_field_value, local_added_int);
2✔
636
            })
2✔
637
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
638
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
639
                sync::SubscriptionSet::State actual =
2✔
640
                    remote_realm->get_latest_subscription_set()
2✔
641
                        .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
642
                        .get();
2✔
643
                REQUIRE(actual == sync::SubscriptionSet::State::Complete);
2!
644
            })
2✔
645
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
646
                wait_for_advance(*local_realm);
2✔
647
                ClientResyncMode mode = client_reset_future.get();
2✔
648
                REQUIRE(mode == ClientResyncMode::Recover);
2!
649
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
650
                auto str_col = table->get_column_key("queryable_str_field");
2✔
651
                auto int_col = table->get_column_key("queryable_int_field");
2✔
652
                auto tv = table->where().equal(str_col, StringData(str_field_value)).find_all();
2✔
653
                tv.sort(int_col);
2✔
654
                // the object we created while offline was recovered, and the remote object was downloaded
655
                REQUIRE(tv.size() == 2);
2!
656
                CHECK(tv.get_object(0).get<Int>(int_col) == local_added_int);
2!
657
                CHECK(tv.get_object(1).get<Int>(int_col) == remote_added_int);
2!
658
            })
2✔
659
            ->run();
2✔
660
    }
2✔
661

662
    SECTION("Recover: subscription and offline writes after client reset failure") {
46✔
663
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
664
        auto&& [error_future, error_handler] = make_error_handler();
2✔
665
        config_local.sync_config->error_handler = error_handler;
2✔
666

667
        std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(config_local.path);
2✔
668
        // create a non-empty directory that we'll fail to delete
669
        util::make_dir(fresh_path);
2✔
670
        util::File(util::File::resolve("file", fresh_path), util::File::mode_Write);
2✔
671

672
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
673
        test_reset
2✔
674
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
675
                auto mut_sub = local_realm->get_latest_subscription_set().make_mutable_copy();
2✔
676
                auto table = local_realm->read_group().get_table("class_TopLevel2");
2✔
677
                mut_sub.insert_or_assign(Query(table));
2✔
678
                mut_sub.commit();
2✔
679

680
                CppContext c(local_realm);
2✔
681
                local_realm->begin_transaction();
2✔
682
                Object::create(c, local_realm, "TopLevel2",
2✔
683
                               std::any(AnyDict{{"_id"s, ObjectId::gen()}, {"queryable_str_field"s, "foo"s}}));
2✔
684
                local_realm->commit_transaction();
2✔
685
            })
2✔
686
            ->on_post_reset([](SharedRealm local_realm) {
2✔
687
                // Verify offline subscription was not removed.
688
                auto subs = local_realm->get_latest_subscription_set();
2✔
689
                auto table = local_realm->read_group().get_table("class_TopLevel2");
2✔
690
                REQUIRE(subs.find(Query(table)));
2!
691
            })
2✔
692
            ->run();
2✔
693

694
        // Remove the folder preventing the completion of a client reset.
695
        util::try_remove_dir_recursive(fresh_path);
2✔
696

697
        RealmConfig config_copy = config_local;
2✔
698
        config_copy.sync_config = std::make_shared<SyncConfig>(*config_copy.sync_config);
2✔
699
        config_copy.sync_config->error_handler = nullptr;
2✔
700
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
701
        config_copy.sync_config->notify_after_client_reset = reset_handler;
2✔
702

703
        // Attempt to open the realm again.
704
        // This time the client reset succeeds and the offline subscription and writes are recovered.
705
        auto realm = Realm::get_shared_realm(config_copy);
2✔
706
        ClientResyncMode mode = reset_future.get();
2✔
707
        REQUIRE(mode == ClientResyncMode::Recover);
2!
708

709
        auto table = realm->read_group().get_table("class_TopLevel2");
2✔
710
        auto str_col = table->get_column_key("queryable_str_field");
2✔
711
        REQUIRE(table->size() == 1);
2!
712
        REQUIRE(table->get_object(0).get<String>(str_col) == "foo");
2!
713
    }
2✔
714

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

756
    SECTION("Recover: offline writes interleaved with subscriptions and empty writes") {
46✔
757
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
758
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
759
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
760
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
761
        test_reset
2✔
762
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
763
                // The sequence of events bellow generates five changesets:
764
                //  1. create sub1 => empty changeset
765
                //  2. create local_added_int object
766
                //  3. create empty changeset
767
                //  4. create sub2 => empty changeset
768
                //  5. create local_added_int2 object
769
                //
770
                // Before sending 'sub2' to the server, an UPLOAD message is sent first.
771
                // The upload message contains changeset 2. (local_added_int) with the cursor
772
                // of changeset 3. (empty changeset).
773

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

813
    auto validate_integrity_of_arrays = [](TableRef table) -> size_t {
46✔
814
        auto sum_col = table->get_column_key("sum_of_list_field");
16✔
815
        auto array_col = table->get_column_key("list_of_ints_field");
16✔
816
        auto query = table->column<Lst<Int>>(array_col).sum() == table->column<Int>(sum_col) &&
16✔
817
                     table->column<Lst<Int>>(array_col).size() > 0;
16✔
818
        return query.count();
16✔
819
    };
16✔
820

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

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

931
    SECTION("unsuccessful replay of local changes") {
46✔
932
        constexpr size_t num_objects_added_before = 2;
4✔
933
        constexpr size_t num_objects_added_after = 2;
4✔
934
        constexpr size_t num_objects_added_by_harness = 1; // BaasFLXClientReset.run()
4✔
935
        constexpr std::string_view added_property_name = "new_property";
4✔
936
        auto&& [error_future, err_handler] = make_error_handler();
4✔
937
        config_local.sync_config->error_handler = err_handler;
4✔
938

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

958
        VersionID expected_version;
4✔
959

960
        auto store_pre_reset_state = [&](SharedRealm local_realm) {
4✔
961
            expected_version = local_realm->read_transaction_version();
4✔
962
        };
4✔
963

964
        auto verify_post_reset_state = [&, err_future = std::move(error_future)](SharedRealm local_realm) {
4✔
965
            auto sync_error = err_future.get();
4✔
966
            REQUIRE(before_reset_count == 1);
4!
967
            REQUIRE(after_reset_count == 0);
4!
968
            REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
4!
969
            REQUIRE(sync_error.is_client_reset_requested());
4!
970

971
            // All changes should have been rolled back when recovery hit remove_column(),
972
            // leaving the Realm in the pre-reset state
973
            local_realm->refresh();
4✔
974
            auto table = local_realm->read_group().get_table("class_TopLevel");
4✔
975
            ColKey added = table->get_column_key(added_property_name);
4✔
976
            REQUIRE(!added);
4!
977
            const size_t expected_added_objects = num_objects_added_before + num_objects_added_after;
4✔
978
            REQUIRE(table->size() == num_objects_added_by_harness + expected_added_objects);
4!
979
            size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
4✔
980
            REQUIRE(count_of_valid_array_data == expected_added_objects);
4!
981

982
            // The attempted client reset should have been recorded so that we
983
            // don't attempt it again
984
            REQUIRE(local_realm->read_transaction_version().version == expected_version.version + 1);
4!
985
        };
4✔
986

987
        SECTION("Recover: unsuccessful recovery leads to a manual reset") {
4✔
988
            config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
989
            auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
990
            test_reset->make_local_changes(make_local_changes_that_will_fail)
2✔
991
                ->on_post_local_changes(store_pre_reset_state)
2✔
992
                ->on_post_reset(std::move(verify_post_reset_state))
2✔
993
                ->run();
2✔
994
            RealmConfig config_copy = config_local;
2✔
995
            auto&& [error_future2, err_handler2] = make_error_handler();
2✔
996
            config_copy.sync_config->error_handler = err_handler2;
2✔
997
            auto realm_post_reset = Realm::get_shared_realm(config_copy);
2✔
998
            auto sync_error = wait_for_future(std::move(error_future2)).get();
2✔
999
            REQUIRE(before_reset_count == 2);
2!
1000
            REQUIRE(after_reset_count == 0);
2!
1001
            REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
1002
            REQUIRE(sync_error.is_client_reset_requested());
2!
1003
        }
2✔
1004

1005
        SECTION("RecoverOrDiscard: unsuccessful reapply leads to discard") {
4✔
1006
            config_local.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1007
            auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1008
            test_reset->make_local_changes(make_local_changes_that_will_fail)
2✔
1009
                ->on_post_local_changes(store_pre_reset_state)
2✔
1010
                ->on_post_reset(std::move(verify_post_reset_state))
2✔
1011
                ->run();
2✔
1012

1013
            RealmConfig config_copy = config_local;
2✔
1014
            auto&& [client_reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
1015
            config_copy.sync_config->error_handler = [](std::shared_ptr<SyncSession>, SyncError err) {
2✔
1016
                REALM_ASSERT_EX(!err.is_fatal, err.status);
×
1017
                CHECK(err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient);
×
1018
            };
×
1019
            config_copy.sync_config->notify_after_client_reset = reset_handler;
2✔
1020
            auto realm_post_reset = Realm::get_shared_realm(config_copy);
2✔
1021
            ClientResyncMode mode = wait_for_future(std::move(client_reset_future)).get();
2✔
1022
            REQUIRE(mode == ClientResyncMode::DiscardLocal);
2!
1023
            realm_post_reset->refresh();
2✔
1024
            auto table = realm_post_reset->read_group().get_table("class_TopLevel");
2✔
1025
            ColKey added = table->get_column_key(added_property_name);
2✔
1026
            REQUIRE(!added);                                        // reverted local changes
2!
1027
            REQUIRE(table->size() == num_objects_added_by_harness); // discarded all offline local changes
2!
1028
        }
2✔
1029
    }
4✔
1030

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

1060
                // adding data and subscriptions to a reset Realm works as normal
1061
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
1062
                auto latest_subs = local_realm->get_latest_subscription_set();
2✔
1063
                REQUIRE(latest_subs.version() > subs.version());
2!
1064
                wait_for_future(latest_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete))
2✔
1065
                    .get();
2✔
1066
                local_realm->refresh();
2✔
1067
                count_of_foo = count_queries_with_str(latest_subs, util::format("\"%1\"", str_field_value));
2✔
1068
                REQUIRE(count_of_foo == 1);
2!
1069
                tv = table->where().equal(queryable_str_field, StringData(str_field_value)).find_all();
2✔
1070
                REQUIRE(tv.size() == 2);
2!
1071
                tv.sort(queryable_int_field);
2✔
1072
                REQUIRE(tv.get_object(0).get<int64_t>(queryable_int_field) == local_added_int);
2!
1073
                REQUIRE(tv.get_object(1).get<int64_t>(queryable_int_field) == remote_added_int);
2!
1074
            })
2✔
1075
            ->run();
2✔
1076
    }
2✔
1077

1078
    SECTION("DiscardLocal: an invalid subscription made while offline becomes superseded") {
46✔
1079
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1080
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
1081
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1082
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1083
        std::unique_ptr<sync::SubscriptionSet> invalid_sub;
2✔
1084
        test_reset
2✔
1085
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
1086
                invalid_sub = std::make_unique<sync::SubscriptionSet>(add_invalid_subscription(local_realm));
2✔
1087
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
1088
            })
2✔
1089
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
1090
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
1091
            })
2✔
1092
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
1093
                local_realm->refresh();
2✔
1094
                sync::SubscriptionSet::State actual =
2✔
1095
                    invalid_sub->get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1096
                REQUIRE(actual == sync::SubscriptionSet::State::Superseded);
2!
1097
                ClientResyncMode mode = client_reset_future.get();
2✔
1098
                REQUIRE(mode == ClientResyncMode::DiscardLocal);
2!
1099
            })
2✔
1100
            ->run();
2✔
1101
    }
2✔
1102

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

1149
    SECTION("DiscardLocal: completion callbacks fire after client reset even when there is no data to download") {
46✔
1150
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1151
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
1152
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1153
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1154
        test_reset
2✔
1155
            ->on_post_local_changes([&](SharedRealm realm) {
2✔
1156
                wait_for_upload(*realm);
2✔
1157
                wait_for_download(*realm);
2✔
1158
            })
2✔
1159
            ->run();
2✔
1160
    }
2✔
1161

1162
    SECTION("DiscardLocal: open realm after client reset failure") {
46✔
1163
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1164
        auto&& [error_future, error_handler] = make_error_handler();
2✔
1165
        config_local.sync_config->error_handler = error_handler;
2✔
1166

1167
        std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(config_local.path);
2✔
1168
        // create a non-empty directory that we'll fail to delete
1169
        util::make_dir(fresh_path);
2✔
1170
        util::File(util::File::resolve("file", fresh_path), util::File::mode_Write);
2✔
1171

1172
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1173
        test_reset->run();
2✔
1174

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

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

1183
        auto realm_post_reset = Realm::get_shared_realm(config_local);
2✔
1184
        sync_error = wait_for_future(std::move(err_future)).get();
2✔
1185
        REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
1186
    }
2✔
1187

1188
    enum class ResetMode { NoReset, InitiateClientReset };
46✔
1189
    auto seed_realm = [&harness, &subscribe_to_and_add_objects](RealmConfig config, ResetMode reset_mode) {
46✔
1190
        config.sync_config->error_handler = [path = config.path](std::shared_ptr<SyncSession>, SyncError err) {
26✔
1191
            // ignore spurious failures on this instance
1192
            util::format(std::cout, "spurious error while seeding a Realm at '%1': %2\n", path, err.status);
×
1193
        };
×
1194
        SharedRealm realm = Realm::get_shared_realm(config);
26✔
1195
        subscribe_to_and_add_objects(realm, 1);
26✔
1196
        auto subs = realm->get_latest_subscription_set();
26✔
1197
        auto result = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
26✔
1198
        CHECK(result == sync::SubscriptionSet::State::Complete);
26!
1199
        if (reset_mode == ResetMode::InitiateClientReset) {
26✔
1200
            reset_utils::trigger_client_reset(harness.session().app_session(), realm);
18✔
1201
        }
18✔
1202
        realm->close();
26✔
1203
    };
26✔
1204

1205
    auto setup_reset_handlers_for_schema_validation =
46✔
1206
        [&before_reset_count, &after_reset_count](RealmConfig& config, Schema expected_schema) {
46✔
1207
            auto& sync_config = *config.sync_config;
14✔
1208
            sync_config.error_handler = [](std::shared_ptr<SyncSession>, SyncError err) {
14✔
1209
                FAIL(err.status);
×
1210
            };
×
1211
            sync_config.notify_before_client_reset = [&before_reset_count,
14✔
1212
                                                      expected = expected_schema](SharedRealm frozen_before) {
14✔
1213
                ++before_reset_count;
14✔
1214
                REQUIRE(frozen_before->schema().size() > 0);
14!
1215
                REQUIRE(frozen_before->schema_version() != ObjectStore::NotVersioned);
14!
1216
                REQUIRE(frozen_before->schema() == expected);
14!
1217
            };
14✔
1218

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

1253
    SECTION("Recover: schema indexes match in before and after states") {
46✔
1254
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1255
        // reorder a property such that it does not match the on disk property order
1256
        std::vector<ObjectSchema> local_schema = schema;
2✔
1257
        std::swap(local_schema[0].persisted_properties[1], local_schema[0].persisted_properties[2]);
2✔
1258
        local_schema[0].persisted_properties.push_back(
2✔
1259
            {"queryable_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
2✔
1260
        config_local.schema = local_schema;
2✔
1261
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1262
        auto future = setup_reset_handlers_for_schema_validation(config_local, local_schema);
2✔
1263
        SharedRealm realm = Realm::get_shared_realm(config_local);
2✔
1264
        future.get();
2✔
1265
        CHECK(before_reset_count == 1);
2!
1266
        CHECK(after_reset_count == 1);
2!
1267
    }
2✔
1268

1269
    SECTION("Adding a local property matching a server addition is allowed") {
46✔
1270
        auto mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
4✔
1271
        config_local.sync_config->client_resync_mode = mode;
4✔
1272
        CHECK_NOTHROW(seed_realm(config_local, ResetMode::InitiateClientReset));
4✔
1273
        std::vector<ObjectSchema> changed_schema = schema;
4✔
1274
        changed_schema[0].persisted_properties.push_back(
4✔
1275
            {"queryable_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
4✔
1276
        // In a separate Realm, make the property addition.
1277
        // Since this is dev mode, it will be added to the server's schema.
1278
        config_remote.schema = changed_schema;
4✔
1279
        CHECK_NOTHROW(seed_realm(config_remote, ResetMode::NoReset));
4✔
1280
        std::swap(changed_schema[0].persisted_properties[1], changed_schema[0].persisted_properties[2]);
4✔
1281
        config_local.schema = changed_schema;
4✔
1282
        auto future = setup_reset_handlers_for_schema_validation(config_local, changed_schema);
4✔
1283
        successfully_async_open_realm(config_local);
4✔
1284
        CHECK_NOTHROW(future.get());
4✔
1285
        CHECK(before_reset_count == 1);
4!
1286
        CHECK(after_reset_count == 1);
4!
1287
    }
4✔
1288

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

1305
        auto notify_before = std::move(config_local.sync_config->notify_before_client_reset);
4✔
1306
        config_local.sync_config->notify_before_client_reset = [=](std::shared_ptr<Realm> realm) {
4✔
1307
            realm->update_schema(changed_schema);
4✔
1308
            notify_before(realm);
4✔
1309
        };
4✔
1310

1311
        auto notify_after = std::move(config_local.sync_config->notify_after_client_reset);
4✔
1312
        config_local.sync_config->notify_after_client_reset = [=](std::shared_ptr<Realm> before,
4✔
1313
                                                                  ThreadSafeReference after, bool did_recover) {
4✔
1314
            before->set_schema_subset(changed_schema);
4✔
1315
            notify_after(before, std::move(after), did_recover);
4✔
1316
        };
4✔
1317

1318
        successfully_async_open_realm(config_local);
4✔
1319
        future.get();
4✔
1320
        CHECK(before_reset_count == 1);
4!
1321
        CHECK(after_reset_count == 1);
4!
1322
    }
4✔
1323

1324
    auto make_additive_changes = [](std::vector<ObjectSchema> schema) {
46✔
1325
        schema[0].persisted_properties.push_back(
6✔
1326
            {"added_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
6✔
1327
        std::swap(schema[0].persisted_properties[1], schema[0].persisted_properties[2]);
6✔
1328
        schema.push_back({"AddedClass",
6✔
1329
                          {
6✔
1330
                              {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
6✔
1331
                              {"str_field", PropertyType::String | PropertyType::Nullable},
6✔
1332
                          }});
6✔
1333
        return schema;
6✔
1334
    };
6✔
1335
    SECTION("Recover: additive schema changes are recovered in dev mode") {
46✔
1336
        const AppSession& app_session = harness.session().app_session();
2✔
1337
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, true);
2✔
1338
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1339
        std::vector<ObjectSchema> changed_schema = make_additive_changes(schema);
2✔
1340
        config_local.schema = changed_schema;
2✔
1341
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1342
        ThreadSafeReference ref_async;
2✔
1343
        auto future = setup_reset_handlers_for_schema_validation(config_local, changed_schema);
2✔
1344
        {
2✔
1345
            auto realm = successfully_async_open_realm(config_local);
2✔
1346
            future.get();
2✔
1347
            CHECK(before_reset_count == 1);
2!
1348
            CHECK(after_reset_count == 1);
2!
1349

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

1403
    SECTION("DiscardLocal: additive schema changes not allowed") {
46✔
1404
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1405
        std::vector<ObjectSchema> changed_schema = make_additive_changes(schema);
2✔
1406
        config_local.schema = changed_schema;
2✔
1407
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1408
        auto&& [error_future, err_handler] = make_error_handler();
2✔
1409
        config_local.sync_config->error_handler = err_handler;
2✔
1410
        auto status = async_open_realm(config_local);
2✔
1411
        REQUIRE_FALSE(status.is_ok());
2!
1412
        REQUIRE_THAT(status.get_status().reason(),
2✔
1413
                     Catch::Matchers::ContainsSubstring(
2✔
1414
                         "'Client reset cannot recover when classes have been removed: {AddedClass}'"));
2✔
1415
        error_future.get();
2✔
1416
        CHECK(before_reset_count == 1);
2!
1417
        CHECK(after_reset_count == 0);
2!
1418
    }
2✔
1419

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

1441
    SECTION("Recover: additive schema changes without dev mode produce an error after client reset") {
46✔
1442
        const AppSession& app_session = harness.session().app_session();
2✔
1443
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, true);
2✔
1444
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1445
        // Disable dev mode so that schema changes are not allowed
1446
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, false);
2✔
1447
        auto cleanup = util::make_scope_exit([&]() noexcept {
2✔
1448
            const AppSession& app_session = harness.session().app_session();
2✔
1449
            app_session.admin_api.set_development_mode_to(app_session.server_app_id, true);
2✔
1450
        });
2✔
1451

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

1481
    SECTION("Recover: inserts in collections in mixed - collections cleared remotely") {
46✔
1482
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1483
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
2✔
1484
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1485
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1486
        test_reset
2✔
1487
            ->populate_initial_object([&](SharedRealm realm) {
2✔
1488
                subscribe_to_all_and_bootstrap(*realm);
2✔
1489
                auto pk_of_added_object = ObjectId::gen();
2✔
1490
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
1491

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

1548
TEST_CASE("flx: creating an object on a class with no subscription throws", "[sync][flx][subscription][baas]") {
2✔
1549
    FLXSyncTestHarness harness("flx_bad_query", {g_simple_embedded_obj_schema, {"queryable_str_field"}});
2✔
1550
    harness.do_with_new_user([&](auto user) {
2✔
1551
        SyncTestFile config(user, harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
1552
        auto [error_promise, error_future] = util::make_promise_future<SyncError>();
2✔
1553
        auto shared_promise = std::make_shared<decltype(error_promise)>(std::move(error_promise));
2✔
1554
        config.sync_config->error_handler = [error_promise = std::move(shared_promise)](std::shared_ptr<SyncSession>,
2✔
1555
                                                                                        SyncError err) {
2✔
1556
            CHECK(err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient);
×
1557
            error_promise->emplace_value(std::move(err));
×
1558
        };
×
1559

1560
        auto realm = Realm::get_shared_realm(config);
2✔
1561
        CppContext c(realm);
2✔
1562
        realm->begin_transaction();
2✔
1563
        REQUIRE_THROWS_AS(
2✔
1564
            Object::create(c, realm, "TopLevel",
2✔
1565
                           std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_str_field", "foo"s}})),
2✔
1566
            NoSubscriptionForWrite);
2✔
1567
        realm->cancel_transaction();
2✔
1568

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

1571
        REQUIRE(table->is_empty());
2!
1572
        auto col_key = table->get_column_key("queryable_str_field");
2✔
1573
        {
2✔
1574
            auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
1575
            new_subs.insert_or_assign(Query(table).equal(col_key, "foo"));
2✔
1576
            auto subs = new_subs.commit();
2✔
1577
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1578
        }
2✔
1579

1580
        realm->begin_transaction();
2✔
1581
        auto obj = Object::create(c, realm, "TopLevel",
2✔
1582
                                  std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
1583
                                                   {"queryable_str_field", "foo"s},
2✔
1584
                                                   {"embedded_obj", AnyDict{{"str_field", "bar"s}}}}));
2✔
1585
        realm->commit_transaction();
2✔
1586

1587
        realm->begin_transaction();
2✔
1588
        auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1589
        embedded_obj.set_property_value(c, "str_field", std::any{"baz"s});
2✔
1590
        realm->commit_transaction();
2✔
1591

1592
        wait_for_upload(*realm);
2✔
1593
        wait_for_download(*realm);
2✔
1594
    });
2✔
1595
}
2✔
1596

1597
TEST_CASE("flx: uploading an object that is out-of-view results in compensating write",
1598
          "[sync][flx][compensating write][baas]") {
16✔
1599
    static std::optional<FLXSyncTestHarness> harness;
16✔
1600
    if (!harness) {
16✔
1601
        Schema schema{{"TopLevel",
2✔
1602
                       {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1603
                        {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1604
                        {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"}}},
2✔
1605
                      {"TopLevel_embedded_obj",
2✔
1606
                       ObjectSchema::ObjectType::Embedded,
2✔
1607
                       {{"str_field", PropertyType::String | PropertyType::Nullable}}},
2✔
1608
                      {"Int PK",
2✔
1609
                       {
2✔
1610
                           {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1611
                           {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1612
                       }},
2✔
1613
                      {"String PK",
2✔
1614
                       {
2✔
1615
                           {"_id", PropertyType::String, Property::IsPrimary{true}},
2✔
1616
                           {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1617
                       }},
2✔
1618
                      {"UUID PK",
2✔
1619
                       {
2✔
1620
                           {"_id", PropertyType::UUID, Property::IsPrimary{true}},
2✔
1621
                           {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1622
                       }}};
2✔
1623

1624
        AppCreateConfig::ServiceRole role{"compensating_write_perms"};
2✔
1625
        role.document_filters.write = {{"queryable_str_field", {{"$in", nlohmann::json::array({"foo", "bar"})}}}};
2✔
1626

1627
        FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}, {role}};
2✔
1628
        harness.emplace("flx_bad_query", server_schema);
2✔
1629
    }
2✔
1630

1631
    create_user_and_log_in(harness->app());
16✔
1632
    auto user = harness->app()->current_user();
16✔
1633

1634
    auto make_error_handler = [] {
16✔
1635
        auto [error_promise, error_future] = util::make_promise_future<SyncError>();
16✔
1636
        auto shared_promise = std::make_shared<decltype(error_promise)>(std::move(error_promise));
16✔
1637
        auto fn = [error_promise = std::move(shared_promise)](std::shared_ptr<SyncSession>, SyncError err) mutable {
16✔
1638
            if (!error_promise) {
14✔
1639
                util::format(std::cerr,
×
1640
                             "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
1641
                             err.status);
×
1642
                abort();
×
1643
            }
×
1644
            error_promise->emplace_value(std::move(err));
14✔
1645
            error_promise.reset();
14✔
1646
        };
14✔
1647

1648
        return std::make_pair(std::move(error_future), std::move(fn));
16✔
1649
    };
16✔
1650

1651
    auto validate_sync_error = [&](const SyncError& sync_error, Mixed expected_pk, const char* expected_object_name,
16✔
1652
                                   const std::string& error_msg_fragment) {
16✔
1653
        CHECK(sync_error.status == ErrorCodes::SyncCompensatingWrite);
14!
1654
        CHECK(!sync_error.is_client_reset_requested());
14!
1655
        CHECK(sync_error.compensating_writes_info.size() == 1);
14!
1656
        CHECK(sync_error.server_requests_action == sync::ProtocolErrorInfo::Action::Warning);
14!
1657
        auto write_info = sync_error.compensating_writes_info[0];
14✔
1658
        CHECK(write_info.primary_key == expected_pk);
14!
1659
        CHECK(write_info.object_name == expected_object_name);
14!
1660
        CHECK_THAT(write_info.reason, Catch::Matchers::ContainsSubstring(error_msg_fragment));
14✔
1661
    };
14✔
1662

1663
    SyncTestFile config(user, harness->schema(), SyncConfig::FLXSyncEnabled{});
16✔
1664
    auto&& [error_future, err_handler] = make_error_handler();
16✔
1665
    config.sync_config->error_handler = err_handler;
16✔
1666
    auto realm = Realm::get_shared_realm(config);
16✔
1667
    auto table = realm->read_group().get_table("class_TopLevel");
16✔
1668

1669
    auto create_subscription = [&](StringData table_name, auto make_query) {
16✔
1670
        auto table = realm->read_group().get_table(table_name);
14✔
1671
        auto queryable_str_field = table->get_column_key("queryable_str_field");
14✔
1672
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
14✔
1673
        new_query.insert_or_assign(make_query(Query(table), queryable_str_field));
14✔
1674
        new_query.commit();
14✔
1675
    };
14✔
1676

1677
    SECTION("compensating write because of permission violation") {
16✔
1678
        create_subscription("class_TopLevel", [](auto q, auto col) {
2✔
1679
            return q.equal(col, "bizz");
2✔
1680
        });
2✔
1681

1682
        CppContext c(realm);
2✔
1683
        realm->begin_transaction();
2✔
1684
        auto invalid_obj = ObjectId::gen();
2✔
1685
        Object::create(c, realm, "TopLevel",
2✔
1686
                       std::any(AnyDict{{"_id", invalid_obj}, {"queryable_str_field", "bizz"s}}));
2✔
1687
        realm->commit_transaction();
2✔
1688

1689
        validate_sync_error(
2✔
1690
            std::move(error_future).get(), invalid_obj, "TopLevel",
2✔
1691
            util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed", invalid_obj.to_string()));
2✔
1692

1693
        wait_for_advance(*realm);
2✔
1694

1695
        auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
1696
        REQUIRE(top_level_table->is_empty());
2!
1697
    }
2✔
1698

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

1704
        CppContext c(realm);
2✔
1705
        realm->begin_transaction();
2✔
1706
        auto invalid_obj = ObjectId::gen();
2✔
1707
        auto obj = Object::create(c, realm, "TopLevel",
2✔
1708
                                  std::any(AnyDict{{"_id", invalid_obj},
2✔
1709
                                                   {"queryable_str_field", "foo"s},
2✔
1710
                                                   {"embedded_obj", AnyDict{{"str_field", "bar"s}}}}));
2✔
1711
        realm->commit_transaction();
2✔
1712
        realm->begin_transaction();
2✔
1713
        obj.set_property_value(c, "queryable_str_field", std::any{"bizz"s});
2✔
1714
        realm->commit_transaction();
2✔
1715
        realm->begin_transaction();
2✔
1716
        auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1717
        embedded_obj.set_property_value(c, "str_field", std::any{"baz"s});
2✔
1718
        realm->commit_transaction();
2✔
1719

1720
        validate_sync_error(
2✔
1721
            std::move(error_future).get(), invalid_obj, "TopLevel",
2✔
1722
            util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed", invalid_obj.to_string()));
2✔
1723

1724
        wait_for_advance(*realm);
2✔
1725

1726
        obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(invalid_obj));
2✔
1727
        embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1728
        REQUIRE(util::any_cast<std::string&&>(obj.get_property_value<std::any>(c, "queryable_str_field")) == "foo");
2!
1729
        REQUIRE(util::any_cast<std::string&&>(embedded_obj.get_property_value<std::any>(c, "str_field")) == "bar");
2!
1730

1731
        realm->begin_transaction();
2✔
1732
        embedded_obj.set_property_value(c, "str_field", std::any{"baz"s});
2✔
1733
        realm->commit_transaction();
2✔
1734

1735
        wait_for_upload(*realm);
2✔
1736
        wait_for_download(*realm);
2✔
1737

1738
        wait_for_advance(*realm);
2✔
1739
        obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(invalid_obj));
2✔
1740
        embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1741
        REQUIRE(embedded_obj.get_column_value<StringData>("str_field") == "baz");
2!
1742
    }
2✔
1743

1744
    SECTION("compensating write for writing a top-level object that is out-of-view") {
16✔
1745
        create_subscription("class_TopLevel", [](auto q, auto col) {
2✔
1746
            return q.equal(col, "foo");
2✔
1747
        });
2✔
1748

1749
        CppContext c(realm);
2✔
1750
        realm->begin_transaction();
2✔
1751
        auto valid_obj = ObjectId::gen();
2✔
1752
        auto invalid_obj = ObjectId::gen();
2✔
1753
        Object::create(c, realm, "TopLevel",
2✔
1754
                       std::any(AnyDict{
2✔
1755
                           {"_id", valid_obj},
2✔
1756
                           {"queryable_str_field", "foo"s},
2✔
1757
                       }));
2✔
1758
        Object::create(c, realm, "TopLevel",
2✔
1759
                       std::any(AnyDict{
2✔
1760
                           {"_id", invalid_obj},
2✔
1761
                           {"queryable_str_field", "bar"s},
2✔
1762
                       }));
2✔
1763
        realm->commit_transaction();
2✔
1764

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

1768
        wait_for_advance(*realm);
2✔
1769

1770
        auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
1771
        REQUIRE(top_level_table->size() == 1);
2!
1772
        REQUIRE(top_level_table->get_object_with_primary_key(valid_obj));
2!
1773

1774
        // Verify that a valid object afterwards does not produce an error
1775
        realm->begin_transaction();
2✔
1776
        Object::create(c, realm, "TopLevel",
2✔
1777
                       std::any(AnyDict{
2✔
1778
                           {"_id", ObjectId::gen()},
2✔
1779
                           {"queryable_str_field", "foo"s},
2✔
1780
                       }));
2✔
1781
        realm->commit_transaction();
2✔
1782

1783
        wait_for_upload(*realm);
2✔
1784
        wait_for_download(*realm);
2✔
1785
    }
2✔
1786

1787
    SECTION("compensating writes for each primary key type") {
16✔
1788
        SECTION("int") {
8✔
1789
            create_subscription("class_Int PK", [](auto q, auto col) {
2✔
1790
                return q.equal(col, "foo");
2✔
1791
            });
2✔
1792
            realm->begin_transaction();
2✔
1793
            realm->read_group().get_table("class_Int PK")->create_object_with_primary_key(123456);
2✔
1794
            realm->commit_transaction();
2✔
1795

1796
            validate_sync_error(std::move(error_future).get(), 123456, "Int PK",
2✔
1797
                                "write to 123456 in table \"Int PK\" not allowed");
2✔
1798
        }
2✔
1799

1800
        SECTION("short string") {
8✔
1801
            create_subscription("class_String PK", [](auto q, auto col) {
2✔
1802
                return q.equal(col, "foo");
2✔
1803
            });
2✔
1804
            realm->begin_transaction();
2✔
1805
            realm->read_group().get_table("class_String PK")->create_object_with_primary_key("short");
2✔
1806
            realm->commit_transaction();
2✔
1807

1808
            validate_sync_error(std::move(error_future).get(), "short", "String PK",
2✔
1809
                                "write to \"short\" in table \"String PK\" not allowed");
2✔
1810
        }
2✔
1811

1812
        SECTION("long string") {
8✔
1813
            create_subscription("class_String PK", [](auto q, auto col) {
2✔
1814
                return q.equal(col, "foo");
2✔
1815
            });
2✔
1816
            realm->begin_transaction();
2✔
1817
            const char* pk = "long string which won't fit in the SSO buffer";
2✔
1818
            realm->read_group().get_table("class_String PK")->create_object_with_primary_key(pk);
2✔
1819
            realm->commit_transaction();
2✔
1820

1821
            validate_sync_error(std::move(error_future).get(), pk, "String PK",
2✔
1822
                                util::format("write to \"%1\" in table \"String PK\" not allowed", pk));
2✔
1823
        }
2✔
1824

1825
        SECTION("uuid") {
8✔
1826
            create_subscription("class_UUID PK", [](auto q, auto col) {
2✔
1827
                return q.equal(col, "foo");
2✔
1828
            });
2✔
1829
            realm->begin_transaction();
2✔
1830
            UUID pk("01234567-9abc-4def-9012-3456789abcde");
2✔
1831
            realm->read_group().get_table("class_UUID PK")->create_object_with_primary_key(pk);
2✔
1832
            realm->commit_transaction();
2✔
1833

1834
            validate_sync_error(std::move(error_future).get(), pk, "UUID PK",
2✔
1835
                                util::format("write to UUID(%1) in table \"UUID PK\" not allowed", pk));
2✔
1836
        }
2✔
1837
    }
8✔
1838

1839
    // Clear the Realm afterwards as we're reusing an app
1840
    realm->begin_transaction();
16✔
1841
    table->clear();
16✔
1842
    realm->commit_transaction();
16✔
1843
    wait_for_upload(*realm);
16✔
1844
    realm.reset();
16✔
1845

1846
    // Add new sections before this
1847
    SECTION("teardown") {
16✔
1848
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
1849
        harness.reset();
2✔
1850
    }
2✔
1851
}
16✔
1852

1853
TEST_CASE("flx: query on non-queryable field results in query error message", "[sync][flx][query][baas]") {
8✔
1854
    static std::optional<FLXSyncTestHarness> harness;
8✔
1855
    if (!harness) {
8✔
1856
        harness.emplace("flx_bad_query");
2✔
1857
    }
2✔
1858

1859
    auto create_subscription = [](SharedRealm realm, StringData table_name, StringData column_name, auto make_query) {
10✔
1860
        auto table = realm->read_group().get_table(table_name);
10✔
1861
        auto queryable_field = table->get_column_key(column_name);
10✔
1862
        auto new_query = realm->get_active_subscription_set().make_mutable_copy();
10✔
1863
        new_query.insert_or_assign(make_query(Query(table), queryable_field));
10✔
1864
        return new_query.commit();
10✔
1865
    };
10✔
1866

1867
    auto check_status = [](auto status) {
8✔
1868
        CHECK(!status.is_ok());
8!
1869
        std::string reason = status.get_status().reason();
8✔
1870
        // Depending on the version of baas used, it may return 'Invalid query:' or
1871
        // 'Client provided query with bad syntax:'
1872
        if ((reason.find("Invalid query:") == std::string::npos &&
8✔
1873
             reason.find("Client provided query with bad syntax:") == std::string::npos) ||
8!
1874
            reason.find("\"TopLevel\": key \"non_queryable_field\" is not a queryable field") == std::string::npos) {
8✔
1875
            FAIL(util::format("Error reason did not match expected: `%1`", reason));
×
1876
        }
×
1877
    };
8✔
1878

1879
    SECTION("Good query after bad query") {
8✔
1880
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1881
            auto subs = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1882
                return q.equal(c, "bar");
2✔
1883
            });
2✔
1884
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1885
            check_status(sub_res);
2✔
1886

1887
            CHECK(realm->get_active_subscription_set().version() == 0);
2!
1888
            CHECK(realm->get_latest_subscription_set().version() == 1);
2!
1889

1890
            subs = create_subscription(realm, "class_TopLevel", "queryable_str_field", [](auto q, auto c) {
2✔
1891
                return q.equal(c, "foo");
2✔
1892
            });
2✔
1893
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1894

1895
            CHECK(realm->get_active_subscription_set().version() == 2);
2!
1896
            CHECK(realm->get_latest_subscription_set().version() == 2);
2!
1897
        });
2✔
1898
    }
2✔
1899

1900
    SECTION("Bad query after bad query") {
8✔
1901
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1902
            auto sync_session = realm->sync_session();
2✔
1903
            sync_session->pause();
2✔
1904

1905
            auto subs = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1906
                return q.equal(c, "bar");
2✔
1907
            });
2✔
1908
            auto subs2 = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1909
                return q.equal(c, "bar");
2✔
1910
            });
2✔
1911

1912
            sync_session->resume();
2✔
1913

1914
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1915
            auto sub_res2 =
2✔
1916
                subs2.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1917

1918
            check_status(sub_res);
2✔
1919
            check_status(sub_res2);
2✔
1920

1921
            CHECK(realm->get_active_subscription_set().version() == 0);
2!
1922
            CHECK(realm->get_latest_subscription_set().version() == 2);
2!
1923
        });
2✔
1924
    }
2✔
1925

1926
    // Test for issue #6839, where wait for download after committing a new subscription and then
1927
    // wait for the subscription complete notification was leading to a garbage reason value in the
1928
    // status provided to the subscription complete callback.
1929
    SECTION("Download during bad query") {
8✔
1930
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1931
            // Wait for steady state before committing the new subscription
1932
            REQUIRE(!wait_for_download(*realm));
2!
1933

1934
            auto subs = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1935
                return q.equal(c, "bar");
2✔
1936
            });
2✔
1937
            // Wait for download is actually waiting for the subscription to be applied after it was committed
1938
            REQUIRE(!wait_for_download(*realm));
2!
1939
            // After subscription is complete or fails during wait for download, this function completes
1940
            // without blocking
1941
            auto result = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1942
            // Verify error occurred
1943
            check_status(result);
2✔
1944
        });
2✔
1945
    }
2✔
1946

1947
    // Add new sections before this
1948
    SECTION("teardown") {
8✔
1949
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
1950
        harness.reset();
2✔
1951
    }
2✔
1952
}
8✔
1953

1954
#if REALM_ENABLE_GEOSPATIAL
1955
TEST_CASE("flx: geospatial", "[sync][flx][geospatial][baas]") {
6✔
1956
    static std::optional<FLXSyncTestHarness> harness;
6✔
1957
    if (!harness) {
6✔
1958
        Schema schema{
2✔
1959
            {"restaurant",
2✔
1960
             {
2✔
1961
                 {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1962
                 {"queryable_str_field", PropertyType::String},
2✔
1963
                 {"location", PropertyType::Object | PropertyType::Nullable, "geoPointType"},
2✔
1964
                 {"array", PropertyType::Object | PropertyType::Array, "geoPointType"},
2✔
1965
             }},
2✔
1966
            {"geoPointType",
2✔
1967
             ObjectSchema::ObjectType::Embedded,
2✔
1968
             {
2✔
1969
                 {"type", PropertyType::String},
2✔
1970
                 {"coordinates", PropertyType::Double | PropertyType::Array},
2✔
1971
             }},
2✔
1972
        };
2✔
1973
        FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field", "location"}};
2✔
1974
        harness.emplace("flx_geospatial", server_schema);
2✔
1975
    }
2✔
1976

1977
    auto create_subscription = [](SharedRealm realm, StringData table_name, StringData column_name, auto make_query) {
18✔
1978
        auto table = realm->read_group().get_table(table_name);
18✔
1979
        auto queryable_field = table->get_column_key(column_name);
18✔
1980
        auto new_query = realm->get_active_subscription_set().make_mutable_copy();
18✔
1981
        new_query.insert_or_assign(make_query(Query(table), queryable_field));
18✔
1982
        return new_query.commit();
18✔
1983
    };
18✔
1984

1985
    SECTION("Server supports a basic geowithin FLX query") {
6✔
1986
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1987
            const realm::AppSession& app_session = harness->session().app_session();
2✔
1988
            auto sync_service = app_session.admin_api.get_sync_service(app_session.server_app_id);
2✔
1989

1990
            AdminAPISession::ServiceConfig config =
2✔
1991
                app_session.admin_api.get_config(app_session.server_app_id, sync_service);
2✔
1992
            auto subs = create_subscription(realm, "class_restaurant", "location", [](Query q, ColKey c) {
2✔
1993
                GeoBox area{GeoPoint{0.2, 0.2}, GeoPoint{0.7, 0.7}};
2✔
1994
                Query query = q.get_table()->column<Link>(c).geo_within(area);
2✔
1995
                std::string ser = query.get_description();
2✔
1996
                return query;
2✔
1997
            });
2✔
1998
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1999
            CHECK(sub_res.is_ok());
2!
2000
            CHECK(realm->get_active_subscription_set().version() == 1);
2!
2001
            CHECK(realm->get_latest_subscription_set().version() == 1);
2!
2002
        });
2✔
2003
    }
2✔
2004

2005
    SECTION("geospatial query consistency: local/server/FLX") {
6✔
2006
        harness->do_with_new_user([&](std::shared_ptr<SyncUser> user) {
2✔
2007
            SyncTestFile config(user, harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
2008
            auto error_pf = util::make_promise_future<SyncError>();
2✔
2009
            config.sync_config->error_handler =
2✔
2010
                [promise = std::make_shared<util::Promise<SyncError>>(std::move(error_pf.promise))](
2✔
2011
                    std::shared_ptr<SyncSession>, SyncError error) {
2✔
2012
                    promise->emplace_value(std::move(error));
2✔
2013
                };
2✔
2014

2015
            auto realm = Realm::get_shared_realm(config);
2✔
2016

2017
            auto subs = create_subscription(realm, "class_restaurant", "queryable_str_field", [](Query q, ColKey c) {
2✔
2018
                return q.equal(c, "synced");
2✔
2019
            });
2✔
2020
            auto make_polygon_filter = [&](const GeoPolygon& polygon) -> bson::BsonDocument {
20✔
2021
                bson::BsonArray inner{};
20✔
2022
                REALM_ASSERT_3(polygon.points.size(), ==, 1);
20✔
2023
                for (auto& point : polygon.points[0]) {
94✔
2024
                    inner.push_back(bson::BsonArray{point.longitude, point.latitude});
94✔
2025
                }
94✔
2026
                bson::BsonArray coords;
20✔
2027
                coords.push_back(inner);
20✔
2028
                bson::BsonDocument geo_bson{{{"type", "Polygon"}, {"coordinates", coords}}};
20✔
2029
                bson::BsonDocument filter{
20✔
2030
                    {"location", bson::BsonDocument{{"$geoWithin", bson::BsonDocument{{"$geometry", geo_bson}}}}}};
20✔
2031
                return filter;
20✔
2032
            };
20✔
2033
            auto make_circle_filter = [&](const GeoCircle& circle) -> bson::BsonDocument {
6✔
2034
                bson::BsonArray coords{circle.center.longitude, circle.center.latitude};
6✔
2035
                bson::BsonArray inner;
6✔
2036
                inner.push_back(coords);
6✔
2037
                inner.push_back(circle.radius_radians);
6✔
2038
                bson::BsonDocument filter{
6✔
2039
                    {"location", bson::BsonDocument{{"$geoWithin", bson::BsonDocument{{"$centerSphere", inner}}}}}};
6✔
2040
                return filter;
6✔
2041
            };
6✔
2042
            auto run_query_on_server = [&](const bson::BsonDocument& filter,
2✔
2043
                                           std::optional<std::string> expected_error = {}) -> size_t {
26✔
2044
                auto remote_client = harness->app()->current_user()->mongo_client("BackingDB");
26✔
2045
                auto db = remote_client.db(harness->session().app_session().config.mongo_dbname);
26✔
2046
                auto restaurant_collection = db["restaurant"];
26✔
2047
                bool processed = false;
26✔
2048
                constexpr int64_t limit = 1000;
26✔
2049
                size_t matches = 0;
26✔
2050
                restaurant_collection.count(filter, limit, [&](uint64_t count, util::Optional<AppError> error) {
26✔
2051
                    processed = true;
26✔
2052
                    if (error) {
26✔
2053
                        if (!expected_error) {
12✔
2054
                            util::format(std::cout, "query error: %1\n", error->reason());
×
2055
                            FAIL(error);
×
2056
                        }
×
2057
                        else {
12✔
2058
                            std::string reason = std::string(error->reason());
12✔
2059
                            std::transform(reason.begin(), reason.end(), reason.begin(), toLowerAscii);
12✔
2060
                            std::transform(expected_error->begin(), expected_error->end(), expected_error->begin(),
12✔
2061
                                           toLowerAscii);
12✔
2062
                            auto pos = reason.find(*expected_error);
12✔
2063
                            if (pos == std::string::npos) {
12✔
2064
                                util::format(std::cout, "mismatch error: '%1' and '%2'\n", reason, *expected_error);
×
2065
                                FAIL(reason);
×
2066
                            }
×
2067
                        }
12✔
2068
                    }
12✔
2069
                    matches = size_t(count);
26✔
2070
                });
26✔
2071
                REQUIRE(processed);
26!
2072
                return matches;
26✔
2073
            };
26✔
2074
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
2075
            CHECK(sub_res.is_ok());
2!
2076
            CHECK(realm->get_active_subscription_set().version() == 1);
2!
2077
            CHECK(realm->get_latest_subscription_set().version() == 1);
2!
2078

2079
            CppContext c(realm);
2✔
2080
            int64_t pk = 0;
2✔
2081
            auto add_point = [&](GeoPoint p) {
16✔
2082
                Object::create(
16✔
2083
                    c, realm, "restaurant",
16✔
2084
                    std::any(AnyDict{
16✔
2085
                        {"_id", ++pk},
16✔
2086
                        {"queryable_str_field", "synced"s},
16✔
2087
                        {"location", AnyDict{{"type", "Point"s},
16✔
2088
                                             {"coordinates", std::vector<std::any>{p.longitude, p.latitude}}}}}));
16✔
2089
            };
16✔
2090
            std::vector<GeoPoint> points = {
2✔
2091
                GeoPoint{-74.006, 40.712800000000001},            // New York city
2✔
2092
                GeoPoint{12.568300000000001, 55.676099999999998}, // Copenhagen
2✔
2093
                GeoPoint{12.082599999999999, 55.628},             // ragnarok, Roskilde
2✔
2094
                GeoPoint{-180.1, -90.1},                          // invalid
2✔
2095
                GeoPoint{0, 90},                                  // north pole
2✔
2096
                GeoPoint{-82.68193, 84.74653},                    // northern point that falls within a box later
2✔
2097
                GeoPoint{82.55243, 84.54981}, // another northern point, but on the other side of the pole
2✔
2098
                GeoPoint{2129, 89},           // invalid
2✔
2099
            };
2✔
2100
            constexpr size_t invalids_to_be_compensated = 2; // 4, 8
2✔
2101
            realm->begin_transaction();
2✔
2102
            for (auto& point : points) {
16✔
2103
                add_point(point);
16✔
2104
            }
16✔
2105
            realm->commit_transaction();
2✔
2106
            const auto& error = error_pf.future.get();
2✔
2107
            REQUIRE(!error.is_fatal);
2!
2108
            REQUIRE(error.status == ErrorCodes::SyncCompensatingWrite);
2!
2109
            REQUIRE(error.compensating_writes_info.size() == invalids_to_be_compensated);
2!
2110
            REQUIRE_THAT(error.compensating_writes_info[0].reason,
2✔
2111
                         Catch::Matchers::ContainsSubstring("in table \"restaurant\" will corrupt geojson data"));
2✔
2112
            REQUIRE_THAT(error.compensating_writes_info[1].reason,
2✔
2113
                         Catch::Matchers::ContainsSubstring("in table \"restaurant\" will corrupt geojson data"));
2✔
2114

2115
            {
2✔
2116
                auto table = realm->read_group().get_table("class_restaurant");
2✔
2117
                CHECK(table->size() == points.size());
2!
2118
                Obj obj = table->get_object_with_primary_key(Mixed{1});
2✔
2119
                REQUIRE(obj);
2!
2120
                Geospatial geo = obj.get<Geospatial>("location");
2✔
2121
                REQUIRE(geo.get_type_string() == "Point");
2!
2122
                REQUIRE(geo.get_type() == Geospatial::Type::Point);
2!
2123
                GeoPoint point = geo.get<GeoPoint>();
2✔
2124
                REQUIRE(point.longitude == points[0].longitude);
2!
2125
                REQUIRE(point.latitude == points[0].latitude);
2!
2126
                REQUIRE(!point.get_altitude());
2!
2127
                ColKey location_col = table->get_column_key("location");
2✔
2128
                auto run_query_locally = [&table, &location_col](Geospatial bounds) -> size_t {
26✔
2129
                    Query query = table->column<Link>(location_col).geo_within(Geospatial(bounds));
26✔
2130
                    return query.find_all().size();
26✔
2131
                };
26✔
2132
                auto run_query_as_flx = [&](Geospatial bounds) -> size_t {
14✔
2133
                    size_t num_objects = 0;
14✔
2134
                    harness->do_with_new_realm([&](SharedRealm realm) {
14✔
2135
                        auto subs =
14✔
2136
                            create_subscription(realm, "class_restaurant", "location", [&](Query q, ColKey c) {
14✔
2137
                                return q.get_table()->column<Link>(c).geo_within(Geospatial(bounds));
14✔
2138
                            });
14✔
2139
                        auto sub_res =
14✔
2140
                            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
14✔
2141
                        CHECK(sub_res.is_ok());
14!
2142
                        CHECK(realm->get_active_subscription_set().version() == 1);
14!
2143
                        realm->refresh();
14✔
2144
                        num_objects = realm->get_class("restaurant").num_objects();
14✔
2145
                    });
14✔
2146
                    return num_objects;
14✔
2147
                };
14✔
2148

2149
                reset_utils::wait_for_num_objects_in_atlas(harness->app()->current_user(),
2✔
2150
                                                           harness->session().app_session(), "restaurant",
2✔
2151
                                                           points.size() - invalids_to_be_compensated);
2✔
2152

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

2248
    // Add new sections before this
2249
    SECTION("teardown") {
6✔
2250
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
2251
        harness.reset();
2✔
2252
    }
2✔
2253
}
6✔
2254
#endif // REALM_ENABLE_GEOSPATIAL
2255

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

2259
    std::vector<ObjectId> obj_ids_at_end = fill_large_array_schema(harness);
2✔
2260
    SyncTestFile interrupted_realm_config(harness.app()->current_user(), harness.schema(),
2✔
2261
                                          SyncConfig::FLXSyncEnabled{});
2✔
2262

2263
    {
2✔
2264
        auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2265
        Realm::Config config = interrupted_realm_config;
2✔
2266
        config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2267
        auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
2268
        config.sync_config->on_sync_client_event_hook =
2✔
2269
            [promise = std::move(shared_promise), seen_version_one = false](std::weak_ptr<SyncSession> weak_session,
2✔
2270
                                                                            const SyncClientHookData& data) mutable {
34✔
2271
                if (data.event != SyncClientHookEvent::DownloadMessageReceived) {
34✔
2272
                    return SyncClientHookAction::NoAction;
26✔
2273
                }
26✔
2274

2275
                auto session = weak_session.lock();
8✔
2276
                if (!session) {
8✔
2277
                    return SyncClientHookAction::NoAction;
×
2278
                }
×
2279

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

2285
                REQUIRE(data.query_version == 1);
2!
2286
                REQUIRE(data.batch_state == sync::DownloadBatchState::MoreToCome);
2!
2287
                auto latest_subs = session->get_flx_subscription_store()->get_latest();
2✔
2288
                REQUIRE(latest_subs.version() == 1);
2!
2289
                REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::Bootstrapping);
2!
2290

2291
                session->close();
2✔
2292
                promise->emplace_value();
2✔
2293

2294
                return SyncClientHookAction::TriggerReconnect;
2✔
2295
            };
2✔
2296

2297
        auto realm = Realm::get_shared_realm(config);
2✔
2298
        {
2✔
2299
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2300
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
2301
            mut_subs.insert_or_assign(Query(table));
2✔
2302
            mut_subs.commit();
2✔
2303
        }
2✔
2304

2305
        interrupted.get();
2✔
2306
        realm->sync_session()->shutdown_and_wait();
2✔
2307
    }
2✔
2308

2309
    {
2✔
2310
        // Verify that the file was fully closed
2311
        auto empty = [](auto&) {};
2✔
2312
        REQUIRE(DB::call_with_lock(interrupted_realm_config.path, empty));
2!
2313
    }
2✔
2314

2315
    {
2✔
2316
        DBOptions options;
2✔
2317
        options.encryption_key = test_util::crypt_key();
2✔
2318
        auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
2319
        auto sub_store = sync::SubscriptionStore::create(realm);
2✔
2320
        auto version_info = sub_store->get_version_info();
2✔
2321
        REQUIRE(version_info.active == 0);
2!
2322
        REQUIRE(version_info.latest == 1);
2!
2323
        auto latest_subs = sub_store->get_latest();
2✔
2324
        REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::Bootstrapping);
2!
2325
        REQUIRE(latest_subs.size() == 1);
2!
2326
        REQUIRE(latest_subs.at(0).object_class_name == "TopLevel");
2!
2327
    }
2✔
2328

2329
    auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
2330
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
2331
    realm->get_latest_subscription_set().get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2332
    wait_for_advance(*realm);
2✔
2333
    REQUIRE(table->size() == obj_ids_at_end.size());
2!
2334
    for (auto& id : obj_ids_at_end) {
10✔
2335
        REQUIRE(table->find_primary_key(Mixed{id}));
10!
2336
    }
10✔
2337

2338
    auto active_subs = realm->get_active_subscription_set();
2✔
2339
    auto latest_subs = realm->get_latest_subscription_set();
2✔
2340
    REQUIRE(active_subs.version() == latest_subs.version());
2!
2341
    REQUIRE(active_subs.version() == int64_t(1));
2!
2342
}
2✔
2343

2344
TEST_CASE("flx: dev mode uploads schema before query change", "[sync][flx][query][baas]") {
2✔
2345
    FLXSyncTestHarness::ServerSchema server_schema;
2✔
2346
    auto default_schema = FLXSyncTestHarness::default_server_schema();
2✔
2347
    server_schema.queryable_fields = default_schema.queryable_fields;
2✔
2348
    server_schema.dev_mode_enabled = true;
2✔
2349
    server_schema.schema = Schema{};
2✔
2350

2351
    FLXSyncTestHarness harness("flx_dev_mode", server_schema);
2✔
2352
    auto foo_obj_id = ObjectId::gen();
2✔
2353
    auto bar_obj_id = ObjectId::gen();
2✔
2354
    harness.do_with_new_realm(
2✔
2355
        [&](SharedRealm realm) {
2✔
2356
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
2357
            // auto queryable_str_field = table->get_column_key("queryable_str_field");
2358
            // auto queryable_int_field = table->get_column_key("queryable_int_field");
2359
            auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2360
            new_query.insert_or_assign(Query(table));
2✔
2361
            new_query.commit();
2✔
2362

2363
            CppContext c(realm);
2✔
2364
            realm->begin_transaction();
2✔
2365
            Object::create(c, realm, "TopLevel",
2✔
2366
                           std::any(AnyDict{{"_id", foo_obj_id},
2✔
2367
                                            {"queryable_str_field", "foo"s},
2✔
2368
                                            {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2369
                                            {"non_queryable_field", "non queryable 1"s}}));
2✔
2370
            Object::create(c, realm, "TopLevel",
2✔
2371
                           std::any(AnyDict{{"_id", bar_obj_id},
2✔
2372
                                            {"queryable_str_field", "bar"s},
2✔
2373
                                            {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2374
                                            {"non_queryable_field", "non queryable 2"s}}));
2✔
2375
            realm->commit_transaction();
2✔
2376

2377
            wait_for_upload(*realm);
2✔
2378
        },
2✔
2379
        default_schema.schema);
2✔
2380

2381
    harness.do_with_new_realm(
2✔
2382
        [&](SharedRealm realm) {
2✔
2383
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
2384
            auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
2385
            auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2386
            new_query.insert_or_assign(Query(table).greater_equal(queryable_int_field, int64_t(5)));
2✔
2387
            auto subs = new_query.commit();
2✔
2388
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2389
            wait_for_download(*realm);
2✔
2390
            Results results(realm, table);
2✔
2391

2392
            realm->refresh();
2✔
2393
            CHECK(results.size() == 2);
2!
2394
            CHECK(table->get_object_with_primary_key({foo_obj_id}).is_valid());
2!
2395
            CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
2!
2396
        },
2✔
2397
        default_schema.schema);
2✔
2398
}
2✔
2399

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

2404
    // first we create an object on the server and upload it.
2405
    auto foo_obj_id = ObjectId::gen();
2✔
2406
    harness.load_initial_data([&](SharedRealm realm) {
2✔
2407
        CppContext c(realm);
2✔
2408
        Object::create(c, realm, "TopLevel",
2✔
2409
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
2410
                                        {"queryable_str_field", "foo"s},
2✔
2411
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2412
                                        {"non_queryable_field", "created as initial data seed"s}}));
2✔
2413
    });
2✔
2414

2415
    // Now create another realm and wait for it to be fully synchronized with bootstrap version zero. i.e.
2416
    // our progress counters should be past the history entry containing the object created above.
2417
    auto test_file_config = harness.make_test_file();
2✔
2418
    auto realm = Realm::get_shared_realm(test_file_config);
2✔
2419
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
2420
    auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
2421

2422
    realm->get_latest_subscription_set().get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2423
    wait_for_upload(*realm);
2✔
2424
    wait_for_download(*realm);
2✔
2425

2426
    // Now disconnect the sync session
2427
    realm->sync_session()->pause();
2✔
2428

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

2434
    realm->begin_transaction();
2✔
2435
    CppContext c(realm);
2✔
2436
    Object::create(c, realm, "TopLevel",
2✔
2437
                   std::any(AnyDict{{"_id", foo_obj_id},
2✔
2438
                                    {"queryable_str_field", "foo"s},
2✔
2439
                                    {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2440
                                    {"non_queryable_field", "created locally"s}}));
2✔
2441
    realm->commit_transaction();
2✔
2442

2443
    // Reconnect the sync session and wait for the subscription that moved "foo" into view to be fully synchronized.
2444
    realm->sync_session()->resume();
2✔
2445
    wait_for_upload(*realm);
2✔
2446
    wait_for_download(*realm);
2✔
2447
    subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2448

2449
    wait_for_advance(*realm);
2✔
2450

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

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

2462
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{foo_obj_id});
2✔
2463
        REQUIRE(obj.get_obj().get<int64_t>("queryable_int_field") == 5);
2!
2464
        REQUIRE(obj.get_obj().get<StringData>("non_queryable_field") == "created as initial data seed");
2!
2465
    });
2✔
2466
}
2✔
2467

2468
TEST_CASE("flx: writes work offline", "[sync][flx][baas]") {
2✔
2469
    FLXSyncTestHarness harness("flx_offline_writes");
2✔
2470

2471
    harness.do_with_new_realm([&](SharedRealm realm) {
2✔
2472
        auto sync_session = realm->sync_session();
2✔
2473
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
2474
        auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
2475
        auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
2476
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2477
        new_query.insert_or_assign(Query(table));
2✔
2478
        new_query.commit();
2✔
2479

2480
        auto foo_obj_id = ObjectId::gen();
2✔
2481
        auto bar_obj_id = ObjectId::gen();
2✔
2482

2483
        CppContext c(realm);
2✔
2484
        realm->begin_transaction();
2✔
2485
        Object::create(c, realm, "TopLevel",
2✔
2486
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
2487
                                        {"queryable_str_field", "foo"s},
2✔
2488
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2489
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
2490
        Object::create(c, realm, "TopLevel",
2✔
2491
                       std::any(AnyDict{{"_id", bar_obj_id},
2✔
2492
                                        {"queryable_str_field", "bar"s},
2✔
2493
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2494
                                        {"non_queryable_field", "non queryable 2"s}}));
2✔
2495
        realm->commit_transaction();
2✔
2496

2497
        wait_for_upload(*realm);
2✔
2498
        wait_for_download(*realm);
2✔
2499
        sync_session->pause();
2✔
2500

2501
        // Make it so the subscriptions only match the "foo" object
2502
        {
2✔
2503
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2504
            mut_subs.clear();
2✔
2505
            mut_subs.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
2506
            mut_subs.commit();
2✔
2507
        }
2✔
2508

2509
        // Make foo so that it will match the next subscription update. This checks whether you can do
2510
        // multiple subscription set updates offline and that the last one eventually takes effect when
2511
        // you come back online and fully synchronize.
2512
        {
2✔
2513
            Results results(realm, table);
2✔
2514
            realm->begin_transaction();
2✔
2515
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2516
            foo_obj.set<int64_t>(queryable_int_field, 15);
2✔
2517
            realm->commit_transaction();
2✔
2518
        }
2✔
2519

2520
        // Update our subscriptions so that both foo/bar will be included
2521
        {
2✔
2522
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2523
            mut_subs.clear();
2✔
2524
            mut_subs.insert_or_assign(Query(table).greater_equal(queryable_int_field, static_cast<int64_t>(10)));
2✔
2525
            mut_subs.commit();
2✔
2526
        }
2✔
2527

2528
        // Make foo out of view for the current subscription.
2529
        {
2✔
2530
            Results results(realm, table);
2✔
2531
            realm->begin_transaction();
2✔
2532
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2533
            foo_obj.set<int64_t>(queryable_int_field, 0);
2✔
2534
            realm->commit_transaction();
2✔
2535
        }
2✔
2536

2537
        sync_session->resume();
2✔
2538
        wait_for_upload(*realm);
2✔
2539
        wait_for_download(*realm);
2✔
2540

2541
        realm->refresh();
2✔
2542
        Results results(realm, table);
2✔
2543
        CHECK(results.size() == 1);
2!
2544
        CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
2!
2545
    });
2✔
2546
}
2✔
2547

2548
TEST_CASE("flx: writes work without waiting for sync", "[sync][flx][baas]") {
2✔
2549
    FLXSyncTestHarness harness("flx_offline_writes");
2✔
2550

2551
    harness.do_with_new_realm([&](SharedRealm realm) {
2✔
2552
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
2553
        auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
2554
        auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
2555
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2556
        new_query.insert_or_assign(Query(table));
2✔
2557
        new_query.commit();
2✔
2558

2559
        auto foo_obj_id = ObjectId::gen();
2✔
2560
        auto bar_obj_id = ObjectId::gen();
2✔
2561

2562
        CppContext c(realm);
2✔
2563
        realm->begin_transaction();
2✔
2564
        Object::create(c, realm, "TopLevel",
2✔
2565
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
2566
                                        {"queryable_str_field", "foo"s},
2✔
2567
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2568
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
2569
        Object::create(c, realm, "TopLevel",
2✔
2570
                       std::any(AnyDict{{"_id", bar_obj_id},
2✔
2571
                                        {"queryable_str_field", "bar"s},
2✔
2572
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2573
                                        {"non_queryable_field", "non queryable 2"s}}));
2✔
2574
        realm->commit_transaction();
2✔
2575

2576
        wait_for_upload(*realm);
2✔
2577

2578
        // Make it so the subscriptions only match the "foo" object
2579
        {
2✔
2580
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2581
            mut_subs.clear();
2✔
2582
            mut_subs.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
2583
            mut_subs.commit();
2✔
2584
        }
2✔
2585

2586
        // Make foo so that it will match the next subscription update. This checks whether you can do
2587
        // multiple subscription set updates without waiting and that the last one eventually takes effect when
2588
        // you fully synchronize.
2589
        {
2✔
2590
            Results results(realm, table);
2✔
2591
            realm->begin_transaction();
2✔
2592
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2593
            foo_obj.set<int64_t>(queryable_int_field, 15);
2✔
2594
            realm->commit_transaction();
2✔
2595
        }
2✔
2596

2597
        // Update our subscriptions so that both foo/bar will be included
2598
        {
2✔
2599
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2600
            mut_subs.clear();
2✔
2601
            mut_subs.insert_or_assign(Query(table).greater_equal(queryable_int_field, static_cast<int64_t>(10)));
2✔
2602
            mut_subs.commit();
2✔
2603
        }
2✔
2604

2605
        // Make foo out-of-view for the current subscription.
2606
        {
2✔
2607
            Results results(realm, table);
2✔
2608
            realm->begin_transaction();
2✔
2609
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2610
            foo_obj.set<int64_t>(queryable_int_field, 0);
2✔
2611
            realm->commit_transaction();
2✔
2612
        }
2✔
2613

2614
        wait_for_upload(*realm);
2✔
2615
        wait_for_download(*realm);
2✔
2616

2617
        realm->refresh();
2✔
2618
        Results results(realm, table);
2✔
2619
        CHECK(results.size() == 1);
2!
2620
        Obj obj = results.get(0);
2✔
2621
        CHECK(obj.get_primary_key().get_object_id() == bar_obj_id);
2!
2622
        CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
2!
2623
    });
2✔
2624
}
2✔
2625

2626
TEST_CASE("flx: verify websocket protocol number and prefixes", "[sync][protocol]") {
2✔
2627
    // Update the expected value whenever the protocol version is updated - this ensures
2628
    // that the current protocol version does not change unexpectedly.
2629
    REQUIRE(14 == sync::get_current_protocol_version());
2✔
2630
    // This was updated in Protocol V8 to use '#' instead of '/' to support the Web SDK
2631
    REQUIRE("com.mongodb.realm-sync#" == sync::get_pbs_websocket_protocol_prefix());
2✔
2632
    REQUIRE("com.mongodb.realm-query-sync#" == sync::get_flx_websocket_protocol_prefix());
2✔
2633
}
2✔
2634

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

2641
    {
2✔
2642
        auto orig_realm = Realm::get_shared_realm(config);
2✔
2643
        auto mut_subs = orig_realm->get_latest_subscription_set().make_mutable_copy();
2✔
2644
        mut_subs.insert_or_assign(Query(orig_realm->read_group().get_table("class_TopLevel")));
2✔
2645
        mut_subs.commit();
2✔
2646
        orig_realm->close();
2✔
2647
    }
2✔
2648

2649
    {
2✔
2650
        auto new_realm = Realm::get_shared_realm(config);
2✔
2651
        auto latest_subs = new_realm->get_latest_subscription_set();
2✔
2652
        CHECK(latest_subs.size() == 1);
2!
2653
        latest_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2654
    }
2✔
2655
}
2✔
2656
#endif
2657

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

2663
    auto realm = Realm::get_shared_realm(config);
2✔
2664
    CHECK(!wait_for_download(*realm));
2!
2665
    CHECK(!wait_for_upload(*realm));
2!
2666

2667
    CHECK(!realm->sync_session()->get_flx_subscription_store());
2!
2668

2669
    CHECK_THROWS_AS(realm->get_active_subscription_set(), IllegalOperation);
2✔
2670
    CHECK_THROWS_AS(realm->get_latest_subscription_set(), IllegalOperation);
2✔
2671
}
2✔
2672

2673
TEST_CASE("flx: connect to FLX as PBS returns an error", "[sync][flx][baas]") {
2✔
2674
    FLXSyncTestHarness harness("connect_to_flx_as_pbs");
2✔
2675
    SyncTestFile config(harness.app()->current_user(), bson::Bson{}, harness.schema());
2✔
2676
    std::mutex sync_error_mutex;
2✔
2677
    util::Optional<SyncError> sync_error;
2✔
2678
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
2✔
2679
        std::lock_guard<std::mutex> lk(sync_error_mutex);
2✔
2680
        sync_error = std::move(error);
2✔
2681
    };
2✔
2682
    auto realm = Realm::get_shared_realm(config);
2✔
2683
    timed_wait_for([&] {
2,825✔
2684
        std::lock_guard<std::mutex> lk(sync_error_mutex);
2,825✔
2685
        return static_cast<bool>(sync_error);
2,825✔
2686
    });
2,825✔
2687

2688
    CHECK(sync_error->status == ErrorCodes::WrongSyncType);
2!
2689
    CHECK(sync_error->server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
2690
}
2✔
2691

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

2697
    REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
2698
                      "Cannot specify a partition value when flexible sync is enabled");
2✔
2699
}
2✔
2700

2701
TEST_CASE("flx: connect to PBS as FLX returns an error", "[sync][flx][protocol][baas]") {
2✔
2702
    auto server_app_config = minimal_app_config("flx_connect_as_pbs", g_minimal_schema);
2✔
2703
    TestAppSession session(create_app(server_app_config));
2✔
2704
    auto app = session.app();
2✔
2705
    auto user = app->current_user();
2✔
2706

2707
    SyncTestFile config(user, g_minimal_schema, SyncConfig::FLXSyncEnabled{});
2✔
2708

2709
    std::mutex sync_error_mutex;
2✔
2710
    util::Optional<SyncError> sync_error;
2✔
2711
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
2✔
2712
        std::lock_guard lk(sync_error_mutex);
2✔
2713
        sync_error = std::move(error);
2✔
2714
    };
2✔
2715
    auto realm = Realm::get_shared_realm(config);
2✔
2716
    timed_wait_for([&] {
29,923✔
2717
        std::lock_guard lk(sync_error_mutex);
29,923✔
2718
        return static_cast<bool>(sync_error);
29,923✔
2719
    });
29,923✔
2720

2721
    CHECK(sync_error->status == ErrorCodes::WrongSyncType);
2!
2722
    CHECK(sync_error->server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
2723
}
2✔
2724

2725
TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][flx][token][baas]") {
2✔
2726
    auto transport = std::make_shared<HookedTransport<>>();
2✔
2727
    FLXSyncTestHarness harness("flx_wait_access_token2", FLXSyncTestHarness::default_server_schema(), transport);
2✔
2728
    auto app = harness.app();
2✔
2729
    std::shared_ptr<User> user = app->current_user();
2✔
2730
    REQUIRE(user);
2!
2731
    REQUIRE(!user->access_token_refresh_required());
2!
2732
    // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client.
2733
    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
2✔
2734
    using namespace std::chrono_literals;
2✔
2735
    auto expires = std::chrono::system_clock::to_time_t(now - 30s);
2✔
2736
    user->update_data_for_testing([&](UserData& data) {
2✔
2737
        data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", expires));
2✔
2738
    });
2✔
2739
    REQUIRE(user->access_token_refresh_required());
2!
2740

2741
    bool seen_waiting_for_access_token = false;
2✔
2742
    // Commit a subcription set while there is no sync session.
2743
    // A session is created when the access token is refreshed.
2744
    transport->request_hook = [&](const Request&) {
2✔
2745
        auto user = app->current_user();
2✔
2746
        REQUIRE(user);
2!
2747
        for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) {
2✔
2748
            if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2749
                REQUIRE(!seen_waiting_for_access_token);
2!
2750
                seen_waiting_for_access_token = true;
2✔
2751

2752
                auto store = session->get_flx_subscription_store();
2✔
2753
                REQUIRE(store);
2!
2754
                auto mut_subs = store->get_latest().make_mutable_copy();
2✔
2755
                mut_subs.commit();
2✔
2756
            }
2✔
2757
        }
2✔
2758
        return std::nullopt;
2✔
2759
    };
2✔
2760
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
2761
    // This triggers the token refresh.
2762
    auto r = Realm::get_shared_realm(config);
2✔
2763
    REQUIRE(seen_waiting_for_access_token);
2!
2764
}
2✔
2765

2766
TEST_CASE("flx: bootstrap batching prevents orphan documents", "[sync][flx][bootstrap][baas]") {
8✔
2767
    struct NovelException : public std::exception {
8✔
2768
        const char* what() const noexcept override
8✔
2769
        {
8✔
2770
            return "Oh no, a really weird exception happened!";
2✔
2771
        }
2✔
2772
    };
8✔
2773

2774
    FLXSyncTestHarness harness("flx_bootstrap_batching", {g_large_array_schema, {"queryable_int_field"}});
8✔
2775

2776
    std::vector<ObjectId> obj_ids_at_end = fill_large_array_schema(harness);
8✔
2777
    SyncTestFile interrupted_realm_config(harness.app()->current_user(), harness.schema(),
8✔
2778
                                          SyncConfig::FLXSyncEnabled{});
8✔
2779

2780
    auto check_interrupted_state = [&](const DBRef& realm) {
8✔
2781
        auto tr = realm->start_read();
8✔
2782
        auto top_level = tr->get_table("class_TopLevel");
8✔
2783
        REQUIRE(top_level);
8!
2784
        REQUIRE(top_level->is_empty());
8!
2785

2786
        auto sub_store = sync::SubscriptionStore::create(realm);
8✔
2787
        auto version_info = sub_store->get_version_info();
8✔
2788
        REQUIRE(version_info.latest == 1);
8!
2789
        REQUIRE(version_info.active == 0);
8!
2790
        auto latest_subs = sub_store->get_latest();
8✔
2791
        REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::Bootstrapping);
8!
2792
        REQUIRE(latest_subs.size() == 1);
8!
2793
        REQUIRE(latest_subs.at(0).object_class_name == "TopLevel");
8!
2794
    };
8✔
2795

2796
    auto peek_pending_state = [](const DBRef& db) {
8✔
2797
        auto logger = util::Logger::get_default_logger();
8✔
2798
        sync::PendingBootstrapStore bootstrap_store(db, *logger, nullptr);
8✔
2799
        REQUIRE(bootstrap_store.has_pending());
8!
2800
        return bootstrap_store.peek_pending(*db->start_read(), 1024 * 1024 * 16);
8✔
2801
    };
8✔
2802

2803
    auto mutate_realm = [&] {
8✔
2804
        harness.load_initial_data([&](SharedRealm realm) {
4✔
2805
            auto table = realm->read_group().get_table("class_TopLevel");
4✔
2806
            Results res(realm, Query(table).greater(table->get_column_key("queryable_int_field"), int64_t(10)));
4✔
2807
            REQUIRE(res.size() == 2);
4!
2808
            res.clear();
4✔
2809
        });
4✔
2810
    };
4✔
2811

2812
    SECTION("unknown exception occurs during bootstrap application on session startup") {
8✔
2813
        {
2✔
2814
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2815
            Realm::Config config = interrupted_realm_config;
2✔
2816
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2817
            auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
2818
            config.sync_config->on_sync_client_event_hook =
2✔
2819
                [promise = std::move(shared_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
2820
                                                      const SyncClientHookData& data) mutable {
52✔
2821
                    if (data.event != SyncClientHookEvent::BootstrapMessageProcessed) {
52✔
2822
                        return SyncClientHookAction::NoAction;
38✔
2823
                    }
38✔
2824
                    auto session = weak_session.lock();
14✔
2825
                    if (!session) {
14✔
2826
                        return SyncClientHookAction::NoAction;
×
2827
                    }
×
2828

2829
                    if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::LastInBatch) {
14✔
2830
                        session->close();
2✔
2831
                        promise->emplace_value();
2✔
2832
                        return SyncClientHookAction::EarlyReturn;
2✔
2833
                    }
2✔
2834
                    return SyncClientHookAction::NoAction;
12✔
2835
                };
14✔
2836
            auto realm = Realm::get_shared_realm(config);
2✔
2837
            {
2✔
2838
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2839
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
2840
                mut_subs.insert_or_assign(Query(table));
2✔
2841
                mut_subs.commit();
2✔
2842
            }
2✔
2843

2844
            interrupted.get();
2✔
2845
            realm->sync_session()->shutdown_and_wait();
2✔
2846
            realm->close();
2✔
2847
        }
2✔
2848

2849
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
2850

2851
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
2852
        // we expected it to be in.
2853
        {
2✔
2854
            DBOptions options;
2✔
2855
            options.encryption_key = test_util::crypt_key();
2✔
2856
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
2857
            auto pending_batch = peek_pending_state(realm);
2✔
2858
            REQUIRE(pending_batch.query_version == 1);
2!
2859
            REQUIRE(pending_batch.progress);
2!
2860

2861
            check_interrupted_state(realm);
2✔
2862
        }
2✔
2863

2864
        auto error_pf = util::make_promise_future<SyncError>();
2✔
2865
        interrupted_realm_config.sync_config->error_handler =
2✔
2866
            [promise = std::make_shared<util::Promise<SyncError>>(std::move(error_pf.promise))](
2✔
2867
                std::shared_ptr<SyncSession>, SyncError error) {
2✔
2868
                promise->emplace_value(std::move(error));
2✔
2869
            };
2✔
2870

2871
        interrupted_realm_config.sync_config->on_sync_client_event_hook =
2✔
2872
            [&, download_message_received = false](std::weak_ptr<SyncSession>,
2✔
2873
                                                   const SyncClientHookData& data) mutable {
8✔
2874
                if (data.event == SyncClientHookEvent::DownloadMessageReceived) {
8✔
2875
                    download_message_received = true;
×
2876
                }
×
2877
                if (data.event != SyncClientHookEvent::BootstrapBatchAboutToProcess) {
8✔
2878
                    return SyncClientHookAction::NoAction;
6✔
2879
                }
6✔
2880

2881
                REQUIRE(!download_message_received);
2!
2882
                throw NovelException{};
2✔
2883
                return SyncClientHookAction::NoAction;
×
2884
            };
2✔
2885

2886
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
2887
        const auto& error = error_pf.future.get();
2✔
2888
        REQUIRE(!error.is_fatal);
2!
2889
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::Warning);
2!
2890
        REQUIRE(error.status == ErrorCodes::UnknownError);
2!
2891
        REQUIRE_THAT(error.status.reason(),
2✔
2892
                     Catch::Matchers::ContainsSubstring("Oh no, a really weird exception happened!"));
2✔
2893
    }
2✔
2894

2895
    SECTION("exception occurs during bootstrap application") {
8✔
2896
        Status error_status(ErrorCodes::OutOfMemory, "no more memory!");
2✔
2897
        {
2✔
2898
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2899
            Realm::Config config = interrupted_realm_config;
2✔
2900
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2901
            config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
2✔
2902
                                                                const SyncClientHookData& data) mutable {
54✔
2903
                if (data.event != SyncClientHookEvent::BootstrapBatchAboutToProcess) {
54✔
2904
                    return SyncClientHookAction::NoAction;
50✔
2905
                }
50✔
2906
                auto session = weak_session.lock();
4✔
2907
                if (!session) {
4✔
2908
                    return SyncClientHookAction::NoAction;
×
2909
                }
×
2910

2911
                if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::MoreToCome) {
4✔
2912
                    throw sync::IntegrationException(error_status);
2✔
2913
                }
2✔
2914
                return SyncClientHookAction::NoAction;
2✔
2915
            };
4✔
2916
            auto error_pf = util::make_promise_future<SyncError>();
2✔
2917
            config.sync_config->error_handler =
2✔
2918
                [promise = std::make_shared<util::Promise<SyncError>>(std::move(error_pf.promise))](
2✔
2919
                    std::shared_ptr<SyncSession>, SyncError error) {
2✔
2920
                    promise->emplace_value(std::move(error));
2✔
2921
                };
2✔
2922

2923

2924
            auto realm = Realm::get_shared_realm(config);
2✔
2925
            {
2✔
2926
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2927
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
2928
                mut_subs.insert_or_assign(Query(table));
2✔
2929
                mut_subs.commit();
2✔
2930
            }
2✔
2931

2932
            auto error = error_pf.future.get();
2✔
2933
            REQUIRE(error.status.reason() == error_status.reason());
2!
2934
            REQUIRE(error.status == error_status);
2!
2935
            realm->sync_session()->shutdown_and_wait();
2✔
2936
            realm->close();
2✔
2937
        }
2✔
2938

2939
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
2940

2941
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
2942
        // we expected it to be in.
2943
        {
2✔
2944
            DBOptions options;
2✔
2945
            options.encryption_key = test_util::crypt_key();
2✔
2946
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
2947
            auto pending_batch = peek_pending_state(realm);
2✔
2948
            REQUIRE(pending_batch.query_version == 1);
2!
2949
            REQUIRE(pending_batch.progress);
2!
2950

2951
            check_interrupted_state(realm);
2✔
2952
        }
2✔
2953

2954
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
2955
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
2956
        realm->get_latest_subscription_set()
2✔
2957
            .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
2958
            .get();
2✔
2959

2960
        wait_for_advance(*realm);
2✔
2961

2962
        REQUIRE(table->size() == obj_ids_at_end.size());
2!
2963
        for (auto& id : obj_ids_at_end) {
10✔
2964
            REQUIRE(table->find_primary_key(Mixed{id}));
10!
2965
        }
10✔
2966
    }
2✔
2967

2968
    SECTION("interrupted before final bootstrap message") {
8✔
2969
        {
2✔
2970
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2971
            Realm::Config config = interrupted_realm_config;
2✔
2972
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2973
            auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
2974
            config.sync_config->on_sync_client_event_hook =
2✔
2975
                [promise = std::move(shared_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
2976
                                                      const SyncClientHookData& data) mutable {
32✔
2977
                    if (data.event != SyncClientHookEvent::BootstrapMessageProcessed) {
32✔
2978
                        return SyncClientHookAction::NoAction;
28✔
2979
                    }
28✔
2980
                    auto session = weak_session.lock();
4✔
2981
                    if (!session) {
4✔
2982
                        return SyncClientHookAction::NoAction;
×
2983
                    }
×
2984

2985
                    if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::MoreToCome) {
4✔
2986
                        session->force_close();
2✔
2987
                        promise->emplace_value();
2✔
2988
                        return SyncClientHookAction::TriggerReconnect;
2✔
2989
                    }
2✔
2990
                    return SyncClientHookAction::NoAction;
2✔
2991
                };
4✔
2992
            auto realm = Realm::get_shared_realm(config);
2✔
2993
            {
2✔
2994
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2995
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
2996
                mut_subs.insert_or_assign(Query(table));
2✔
2997
                mut_subs.commit();
2✔
2998
            }
2✔
2999

3000
            interrupted.get();
2✔
3001
            realm->sync_session()->shutdown_and_wait();
2✔
3002
            realm->close();
2✔
3003
        }
2✔
3004

3005
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
3006

3007
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
3008
        // we expected it to be in.
3009
        {
2✔
3010
            DBOptions options;
2✔
3011
            options.encryption_key = test_util::crypt_key();
2✔
3012
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
3013
            auto pending_batch = peek_pending_state(realm);
2✔
3014
            REQUIRE(pending_batch.query_version == 1);
2!
3015
            REQUIRE(!pending_batch.progress);
2!
3016
            REQUIRE(pending_batch.remaining_changesets == 0);
2!
3017
            REQUIRE(pending_batch.changesets.size() == 1);
2!
3018

3019
            check_interrupted_state(realm);
2✔
3020
        }
2✔
3021

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

3026
        // Finally re-open the realm whose bootstrap we interrupted and just wait for it to finish downloading.
3027
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
3028
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
3029
        realm->get_latest_subscription_set()
2✔
3030
            .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
3031
            .get();
2✔
3032

3033
        wait_for_advance(*realm);
2✔
3034
        auto expected_obj_ids = util::Span<ObjectId>(obj_ids_at_end).sub_span(0, 3);
2✔
3035

3036
        REQUIRE(table->size() == expected_obj_ids.size());
2!
3037
        for (auto& id : expected_obj_ids) {
6✔
3038
            REQUIRE(table->find_primary_key(Mixed{id}));
6!
3039
        }
6✔
3040
    }
2✔
3041

3042
    SECTION("interrupted after final bootstrap message before processing") {
8✔
3043
        {
2✔
3044
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
3045
            Realm::Config config = interrupted_realm_config;
2✔
3046
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
3047
            auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
3048
            config.sync_config->on_sync_client_event_hook =
2✔
3049
                [promise = std::move(shared_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
3050
                                                      const SyncClientHookData& data) mutable {
52✔
3051
                    if (data.event != SyncClientHookEvent::BootstrapMessageProcessed) {
52✔
3052
                        return SyncClientHookAction::NoAction;
38✔
3053
                    }
38✔
3054
                    auto session = weak_session.lock();
14✔
3055
                    if (!session) {
14✔
3056
                        return SyncClientHookAction::NoAction;
×
3057
                    }
×
3058

3059
                    if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::LastInBatch) {
14✔
3060
                        session->force_close();
2✔
3061
                        promise->emplace_value();
2✔
3062
                        return SyncClientHookAction::TriggerReconnect;
2✔
3063
                    }
2✔
3064
                    return SyncClientHookAction::NoAction;
12✔
3065
                };
14✔
3066
            auto realm = Realm::get_shared_realm(config);
2✔
3067
            {
2✔
3068
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3069
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
3070
                mut_subs.insert_or_assign(Query(table));
2✔
3071
                mut_subs.commit();
2✔
3072
            }
2✔
3073

3074
            interrupted.get();
2✔
3075
            realm->sync_session()->shutdown_and_wait();
2✔
3076
            realm->close();
2✔
3077
        }
2✔
3078

3079
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
3080

3081
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
3082
        // we expected it to be in.
3083
        {
2✔
3084
            DBOptions options;
2✔
3085
            options.encryption_key = test_util::crypt_key();
2✔
3086
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
3087
            auto pending_batch = peek_pending_state(realm);
2✔
3088
            REQUIRE(pending_batch.query_version == 1);
2!
3089
            REQUIRE(static_cast<bool>(pending_batch.progress));
2!
3090
            REQUIRE(pending_batch.remaining_changesets == 0);
2!
3091
            REQUIRE(pending_batch.changesets.size() == 6);
2!
3092

3093
            check_interrupted_state(realm);
2✔
3094
        }
2✔
3095

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

3100
        auto [saw_valid_state_promise, saw_valid_state_future] = util::make_promise_future<void>();
2✔
3101
        auto shared_saw_valid_state_promise =
2✔
3102
            std::make_shared<decltype(saw_valid_state_promise)>(std::move(saw_valid_state_promise));
2✔
3103
        // This hook will let us check what the state of the realm is before it's integrated any new download
3104
        // messages from the server. This should be the full 5 object bootstrap that was received before we
3105
        // called mutate_realm().
3106
        interrupted_realm_config.sync_config->on_sync_client_event_hook =
2✔
3107
            [&, promise = std::move(shared_saw_valid_state_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
3108
                                                                     const SyncClientHookData& data) {
40✔
3109
                if (data.event != SyncClientHookEvent::DownloadMessageReceived) {
40✔
3110
                    return SyncClientHookAction::NoAction;
38✔
3111
                }
38✔
3112
                auto session = weak_session.lock();
2✔
3113
                if (!session) {
2✔
3114
                    return SyncClientHookAction::NoAction;
×
3115
                }
×
3116

3117
                if (data.query_version != 1 || data.batch_state == sync::DownloadBatchState::MoreToCome) {
2✔
3118
                    return SyncClientHookAction::NoAction;
×
3119
                }
×
3120

3121
                auto latest_sub_set = session->get_flx_subscription_store()->get_latest();
2✔
3122
                auto active_sub_set = session->get_flx_subscription_store()->get_active();
2✔
3123
                auto version_info = session->get_flx_subscription_store()->get_version_info();
2✔
3124
                REQUIRE(version_info.pending_mark == active_sub_set.version());
2!
3125
                REQUIRE(version_info.active == active_sub_set.version());
2!
3126
                REQUIRE(version_info.latest == latest_sub_set.version());
2!
3127
                REQUIRE(latest_sub_set.version() == active_sub_set.version());
2!
3128
                REQUIRE(active_sub_set.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3129

3130
                auto db = SyncSession::OnlyForTesting::get_db(*session);
2✔
3131
                auto tr = db->start_read();
2✔
3132

3133
                auto table = tr->get_table("class_TopLevel");
2✔
3134
                REQUIRE(table->size() == obj_ids_at_end.size());
2!
3135
                for (auto& id : obj_ids_at_end) {
10✔
3136
                    REQUIRE(table->find_primary_key(Mixed{id}));
10!
3137
                }
10✔
3138

3139
                promise->emplace_value();
2✔
3140
                return SyncClientHookAction::NoAction;
2✔
3141
            };
2✔
3142

3143
        // Finally re-open the realm whose bootstrap we interrupted and just wait for it to finish downloading.
3144
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
3145
        saw_valid_state_future.get();
2✔
3146
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
3147
        realm->get_latest_subscription_set()
2✔
3148
            .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
3149
            .get();
2✔
3150

3151
        wait_for_advance(*realm);
2✔
3152
        auto expected_obj_ids = util::Span<ObjectId>(obj_ids_at_end).sub_span(0, 3);
2✔
3153

3154
        // After we've downloaded all the mutations there should only by 3 objects left.
3155
        REQUIRE(table->size() == expected_obj_ids.size());
2!
3156
        for (auto& id : expected_obj_ids) {
6✔
3157
            REQUIRE(table->find_primary_key(Mixed{id}));
6!
3158
        }
6✔
3159
    }
2✔
3160
}
8✔
3161

3162
// Check that a document with the given id is present and has the expected fields
3163
static void check_document(const std::vector<bson::BsonDocument>& documents, ObjectId id,
3164
                           std::initializer_list<std::pair<const char*, bson::Bson>> fields)
3165
{
428✔
3166
    auto it = std::find_if(documents.begin(), documents.end(), [&](auto&& doc) {
43,096✔
3167
        auto val = doc.find("_id");
43,096✔
3168
        REQUIRE(val);
43,096!
3169
        return *val == id;
43,096✔
3170
    });
43,096✔
3171
    REQUIRE(it != documents.end());
428!
3172
    auto& doc = *it;
428✔
3173
    for (auto& [name, expected_value] : fields) {
434✔
3174
        auto val = doc.find(name);
434✔
3175
        REQUIRE(val);
434!
3176

3177
        // bson documents are ordered  but Realm dictionaries aren't, so the
3178
        // document might validly be in a different order than we expected and
3179
        // we need to do a comparison that doesn't check order.
3180
        if (expected_value.type() == bson::Bson::Type::Document) {
434✔
3181
            REQUIRE(static_cast<const bson::BsonDocument&>(*val) ==
8!
3182
                    static_cast<const bson::BsonDocument&>(expected_value));
8✔
3183
        }
8✔
3184
        else {
426✔
3185
            REQUIRE(*val == expected_value);
426!
3186
        }
426✔
3187
    }
434✔
3188
}
428✔
3189

3190
TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") {
22✔
3191
    using namespace ::realm::bson;
22✔
3192

3193
    static auto server_schema = [] {
22✔
3194
        FLXSyncTestHarness::ServerSchema server_schema;
2✔
3195
        server_schema.queryable_fields = {"queryable_str_field"};
2✔
3196
        server_schema.schema = {
2✔
3197
            {"Asymmetric",
2✔
3198
             ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3199
             {
2✔
3200
                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3201
                 {"location", PropertyType::String | PropertyType::Nullable},
2✔
3202
                 {"embedded obj", PropertyType::Object | PropertyType::Nullable, "Embedded"},
2✔
3203
                 {"embedded list", PropertyType::Object | PropertyType::Array, "Embedded"},
2✔
3204
                 {"embedded dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary,
2✔
3205
                  "Embedded"},
2✔
3206
                 {"link obj", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
2✔
3207
                 {"link list", PropertyType::Object | PropertyType::Array, "TopLevel"},
2✔
3208
                 {"link dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary,
2✔
3209
                  "TopLevel"},
2✔
3210
             }},
2✔
3211
            {"Embedded", ObjectSchema::ObjectType::Embedded, {{"value", PropertyType::String}}},
2✔
3212
            {"TopLevel",
2✔
3213
             {
2✔
3214
                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3215
                 {"value", PropertyType::Int},
2✔
3216
             }},
2✔
3217
        };
2✔
3218
        return server_schema;
2✔
3219
    }();
2✔
3220
    static auto harness = std::make_unique<FLXSyncTestHarness>("asymmetric_sync", server_schema);
22✔
3221

3222
    // We reuse a single app for each section, so tests will see the documents
3223
    // created by previous tests and we need to add those documents to the count
3224
    // we're waiting for
3225
    static std::unordered_map<std::string, size_t> previous_count;
22✔
3226
    auto get_documents = [&](const char* name, size_t expected_count) {
22✔
3227
        auto& count = previous_count[name];
18✔
3228
        auto documents =
18✔
3229
            harness->session().get_documents(*harness->app()->current_user(), name, count + expected_count);
18✔
3230
        count = documents.size();
18✔
3231
        return documents;
18✔
3232
    };
18✔
3233

3234
    SECTION("basic object construction") {
22✔
3235
        auto foo_obj_id = ObjectId::gen();
2✔
3236
        auto bar_obj_id = ObjectId::gen();
2✔
3237
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3238
            realm->begin_transaction();
2✔
3239
            CppContext c(realm);
2✔
3240
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}}));
2✔
3241
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}}));
2✔
3242
            realm->commit_transaction();
2✔
3243

3244
            auto documents = get_documents("Asymmetric", 2);
2✔
3245
            check_document(documents, foo_obj_id, {{"location", "foo"}});
2✔
3246
            check_document(documents, bar_obj_id, {{"location", "bar"}});
2✔
3247
        });
2✔
3248

3249
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3250
            wait_for_download(*realm);
2✔
3251

3252
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3253
            REQUIRE(table->size() == 0);
2!
3254
            // Cannot query asymmetric tables.
3255
            CHECK_THROWS_AS(Query(table), LogicError);
2✔
3256
        });
2✔
3257
    }
2✔
3258

3259
    SECTION("do not allow objects with same key within the same transaction") {
22✔
3260
        auto foo_obj_id = ObjectId::gen();
2✔
3261
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3262
            realm->begin_transaction();
2✔
3263
            CppContext c(realm);
2✔
3264
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}}));
2✔
3265
            CHECK_THROWS_WITH(
2✔
3266
                Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "bar"s}})),
2✔
3267
                "Attempting to create an object of type 'Asymmetric' with an existing primary key value 'not "
2✔
3268
                "implemented'");
2✔
3269
            realm->commit_transaction();
2✔
3270

3271
            auto documents = get_documents("Asymmetric", 1);
2✔
3272
            check_document(documents, foo_obj_id, {{"location", "foo"}});
2✔
3273
        });
2✔
3274
    }
2✔
3275

3276
    SECTION("create multiple objects - separate commits") {
22✔
3277
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3278
            CppContext c(realm);
2✔
3279
            std::vector<ObjectId> obj_ids;
2✔
3280
            for (int i = 0; i < 100; ++i) {
202✔
3281
                realm->begin_transaction();
200✔
3282
                obj_ids.push_back(ObjectId::gen());
200✔
3283
                Object::create(c, realm, "Asymmetric",
200✔
3284
                               std::any(AnyDict{
200✔
3285
                                   {"_id", obj_ids.back()},
200✔
3286
                                   {"location", util::format("foo_%1", i)},
200✔
3287
                               }));
200✔
3288
                realm->commit_transaction();
200✔
3289
            }
200✔
3290

3291
            wait_for_upload(*realm);
2✔
3292
            wait_for_download(*realm);
2✔
3293

3294
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3295
            REQUIRE(table->size() == 0);
2!
3296

3297
            auto documents = get_documents("Asymmetric", 100);
2✔
3298
            for (int i = 0; i < 100; ++i) {
202✔
3299
                check_document(documents, obj_ids[i], {{"location", util::format("foo_%1", i)}});
200✔
3300
            }
200✔
3301
        });
2✔
3302
    }
2✔
3303

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

3319
            wait_for_upload(*realm);
2✔
3320
            wait_for_download(*realm);
2✔
3321

3322
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3323
            REQUIRE(table->size() == 0);
2!
3324

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

3332
    SECTION("open with schema mismatch on IsAsymmetric") {
22✔
3333
        auto schema = server_schema.schema;
2✔
3334
        schema.find("Asymmetric")->table_type = ObjectSchema::ObjectType::TopLevel;
2✔
3335

3336
        harness->do_with_new_user([&](std::shared_ptr<SyncUser> user) {
2✔
3337
            SyncTestFile config(user, schema, SyncConfig::FLXSyncEnabled{});
2✔
3338
            auto [error_promise, error_future] = util::make_promise_future<SyncError>();
2✔
3339
            auto error_count = 0;
2✔
3340
            auto err_handler = [promise = util::CopyablePromiseHolder(std::move(error_promise)),
2✔
3341
                                &error_count](std::shared_ptr<SyncSession>, SyncError err) mutable {
4✔
3342
                ++error_count;
4✔
3343
                if (error_count == 1) {
4✔
3344
                    // Bad changeset detected by the client.
3345
                    CHECK(err.status == ErrorCodes::BadChangeset);
2!
3346
                }
2✔
3347
                else if (error_count == 2) {
2✔
3348
                    // Server asking for a client reset.
3349
                    CHECK(err.status == ErrorCodes::SyncClientResetRequired);
2!
3350
                    CHECK(err.is_client_reset_requested());
2!
3351
                    promise.get_promise().emplace_value(std::move(err));
2✔
3352
                }
2✔
3353
            };
4✔
3354

3355
            config.sync_config->error_handler = err_handler;
2✔
3356
            auto realm = Realm::get_shared_realm(config);
2✔
3357

3358
            auto err = error_future.get();
2✔
3359
            CHECK(error_count == 2);
2!
3360
        });
2✔
3361
    }
2✔
3362

3363
    SECTION("basic embedded object construction") {
22✔
3364
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3365
            auto obj_id = ObjectId::gen();
2✔
3366
            realm->begin_transaction();
2✔
3367
            CppContext c(realm);
2✔
3368
            Object::create(c, realm, "Asymmetric",
2✔
3369
                           std::any(AnyDict{
2✔
3370
                               {"_id", obj_id},
2✔
3371
                               {"embedded obj", AnyDict{{"value", "foo"s}}},
2✔
3372
                           }));
2✔
3373
            realm->commit_transaction();
2✔
3374
            wait_for_upload(*realm);
2✔
3375

3376
            auto documents = get_documents("Asymmetric", 1);
2✔
3377
            check_document(documents, obj_id, {{"embedded obj", BsonDocument{{"value", "foo"}}}});
2✔
3378
        });
2✔
3379

3380
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3381
            wait_for_download(*realm);
2✔
3382

3383
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3384
            REQUIRE(table->size() == 0);
2!
3385
        });
2✔
3386
    }
2✔
3387

3388
    SECTION("replace embedded object") {
22✔
3389
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3390
            CppContext c(realm);
2✔
3391
            auto foo_obj_id = ObjectId::gen();
2✔
3392

3393
            realm->begin_transaction();
2✔
3394
            Object::create(c, realm, "Asymmetric",
2✔
3395
                           std::any(AnyDict{
2✔
3396
                               {"_id", foo_obj_id},
2✔
3397
                               {"embedded obj", AnyDict{{"value", "foo"s}}},
2✔
3398
                           }));
2✔
3399
            realm->commit_transaction();
2✔
3400

3401
            // Update embedded field to `null`. The server discards this write
3402
            // as asymmetric sync can only create new objects.
3403
            realm->begin_transaction();
2✔
3404
            Object::create(c, realm, "Asymmetric",
2✔
3405
                           std::any(AnyDict{
2✔
3406
                               {"_id", foo_obj_id},
2✔
3407
                               {"embedded obj", std::any()},
2✔
3408
                           }));
2✔
3409
            realm->commit_transaction();
2✔
3410

3411
            // create a second object so that we can know when the translator
3412
            // has processed everything
3413
            realm->begin_transaction();
2✔
3414
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", ObjectId::gen()}, {}}));
2✔
3415
            realm->commit_transaction();
2✔
3416

3417
            wait_for_upload(*realm);
2✔
3418
            wait_for_download(*realm);
2✔
3419

3420
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3421
            REQUIRE(table->size() == 0);
2!
3422

3423
            auto documents = get_documents("Asymmetric", 2);
2✔
3424
            check_document(documents, foo_obj_id, {{"embedded obj", BsonDocument{{"value", "foo"}}}});
2✔
3425
        });
2✔
3426
    }
2✔
3427

3428
    SECTION("embedded collections") {
22✔
3429
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3430
            CppContext c(realm);
2✔
3431
            auto obj_id = ObjectId::gen();
2✔
3432

3433
            realm->begin_transaction();
2✔
3434
            Object::create(c, realm, "Asymmetric",
2✔
3435
                           std::any(AnyDict{
2✔
3436
                               {"_id", obj_id},
2✔
3437
                               {"embedded list", AnyVector{AnyDict{{"value", "foo"s}}, AnyDict{{"value", "bar"s}}}},
2✔
3438
                               {"embedded dictionary",
2✔
3439
                                AnyDict{
2✔
3440
                                    {"key1", AnyDict{{"value", "foo"s}}},
2✔
3441
                                    {"key2", AnyDict{{"value", "bar"s}}},
2✔
3442
                                }},
2✔
3443
                           }));
2✔
3444
            realm->commit_transaction();
2✔
3445

3446
            auto documents = get_documents("Asymmetric", 1);
2✔
3447
            check_document(
2✔
3448
                documents, obj_id,
2✔
3449
                {
2✔
3450
                    {"embedded list", BsonArray{BsonDocument{{"value", "foo"}}, BsonDocument{{"value", "bar"}}}},
2✔
3451
                    {"embedded dictionary",
2✔
3452
                     BsonDocument{
2✔
3453
                         {"key1", BsonDocument{{"value", "foo"}}},
2✔
3454
                         {"key2", BsonDocument{{"value", "bar"}}},
2✔
3455
                     }},
2✔
3456
                });
2✔
3457
        });
2✔
3458
    }
2✔
3459

3460
    SECTION("asymmetric table not allowed in PBS") {
22✔
3461
        Schema schema{
2✔
3462
            {"Asymmetric2",
2✔
3463
             ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3464
             {
2✔
3465
                 {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
3466
                 {"location", PropertyType::Int},
2✔
3467
                 {"reading", PropertyType::Int},
2✔
3468
             }},
2✔
3469
        };
2✔
3470

3471
        SyncTestFile config(harness->app()->current_user(), Bson{}, schema);
2✔
3472
        REQUIRE_EXCEPTION(
2✔
3473
            Realm::get_shared_realm(config), SchemaValidationFailed,
2✔
3474
            Catch::Matchers::ContainsSubstring("Asymmetric table 'Asymmetric2' not allowed in partition based sync"));
2✔
3475
    }
2✔
3476

3477
    SECTION("links to top-level objects") {
22✔
3478
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3479
            subscribe_to_all_and_bootstrap(*realm);
2✔
3480

3481
            ObjectId obj_id = ObjectId::gen();
2✔
3482
            std::array<ObjectId, 5> target_obj_ids;
2✔
3483
            for (auto& id : target_obj_ids) {
10✔
3484
                id = ObjectId::gen();
10✔
3485
            }
10✔
3486

3487
            realm->begin_transaction();
2✔
3488
            CppContext c(realm);
2✔
3489
            Object::create(c, realm, "Asymmetric",
2✔
3490
                           std::any(AnyDict{
2✔
3491
                               {"_id", obj_id},
2✔
3492
                               {"link obj", AnyDict{{"_id", target_obj_ids[0]}, {"value", INT64_C(10)}}},
2✔
3493
                               {"link list",
2✔
3494
                                AnyVector{
2✔
3495
                                    AnyDict{{"_id", target_obj_ids[1]}, {"value", INT64_C(11)}},
2✔
3496
                                    AnyDict{{"_id", target_obj_ids[2]}, {"value", INT64_C(12)}},
2✔
3497
                                }},
2✔
3498
                               {"link dictionary",
2✔
3499
                                AnyDict{
2✔
3500
                                    {"key1", AnyDict{{"_id", target_obj_ids[3]}, {"value", INT64_C(13)}}},
2✔
3501
                                    {"key2", AnyDict{{"_id", target_obj_ids[4]}, {"value", INT64_C(14)}}},
2✔
3502
                                }},
2✔
3503
                           }));
2✔
3504
            realm->commit_transaction();
2✔
3505
            wait_for_upload(*realm);
2✔
3506

3507
            auto docs1 = get_documents("Asymmetric", 1);
2✔
3508
            check_document(docs1, obj_id,
2✔
3509
                           {{"link obj", target_obj_ids[0]},
2✔
3510
                            {"link list", BsonArray{{target_obj_ids[1], target_obj_ids[2]}}},
2✔
3511
                            {
2✔
3512
                                "link dictionary",
2✔
3513
                                BsonDocument{
2✔
3514
                                    {"key1", target_obj_ids[3]},
2✔
3515
                                    {"key2", target_obj_ids[4]},
2✔
3516
                                },
2✔
3517
                            }});
2✔
3518

3519
            auto docs2 = get_documents("TopLevel", 5);
2✔
3520
            for (int64_t i = 0; i < 5; ++i) {
12✔
3521
                check_document(docs2, target_obj_ids[i], {{"value", 10 + i}});
10✔
3522
            }
10✔
3523
        });
2✔
3524
    }
2✔
3525

3526
    // Add any new test sections above this point
3527

3528
    SECTION("teardown") {
22✔
3529
        harness.reset();
2✔
3530
    }
2✔
3531
}
22✔
3532

3533
TEST_CASE("flx: data ingest - dev mode", "[sync][flx][data ingest][baas]") {
2✔
3534
    FLXSyncTestHarness::ServerSchema server_schema;
2✔
3535
    server_schema.dev_mode_enabled = true;
2✔
3536
    server_schema.schema = Schema{};
2✔
3537

3538
    auto schema = Schema{{"Asymmetric",
2✔
3539
                          ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3540
                          {
2✔
3541
                              {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3542
                              {"location", PropertyType::String | PropertyType::Nullable},
2✔
3543
                          }},
2✔
3544
                         {"TopLevel",
2✔
3545
                          {
2✔
3546
                              {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3547
                              {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
3548
                          }}};
2✔
3549

3550
    FLXSyncTestHarness harness("asymmetric_sync", server_schema);
2✔
3551

3552
    auto foo_obj_id = ObjectId::gen();
2✔
3553
    auto bar_obj_id = ObjectId::gen();
2✔
3554

3555
    harness.do_with_new_realm(
2✔
3556
        [&](SharedRealm realm) {
2✔
3557
            CppContext c(realm);
2✔
3558
            realm->begin_transaction();
2✔
3559
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}}));
2✔
3560
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}}));
2✔
3561
            realm->commit_transaction();
2✔
3562
            User* user = dynamic_cast<User*>(realm->config().sync_config->user.get());
2✔
3563
            REALM_ASSERT(user);
2✔
3564
            auto docs = harness.session().get_documents(*user, "Asymmetric", 2);
2✔
3565
            check_document(docs, foo_obj_id, {{"location", "foo"}});
2✔
3566
            check_document(docs, bar_obj_id, {{"location", "bar"}});
2✔
3567
        },
2✔
3568
        schema);
2✔
3569
}
2✔
3570

3571
TEST_CASE("flx: data ingest - write not allowed", "[sync][flx][data ingest][baas]") {
2✔
3572
    AppCreateConfig::ServiceRole role{"asymmetric_write_perms"};
2✔
3573
    role.document_filters.write = false;
2✔
3574

3575
    Schema schema({
2✔
3576
        {"Asymmetric",
2✔
3577
         ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3578
         {
2✔
3579
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3580
             {"location", PropertyType::String | PropertyType::Nullable},
2✔
3581
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "Embedded"},
2✔
3582
         }},
2✔
3583
        {"Embedded",
2✔
3584
         ObjectSchema::ObjectType::Embedded,
2✔
3585
         {
2✔
3586
             {"value", PropertyType::String | PropertyType::Nullable},
2✔
3587
         }},
2✔
3588
    });
2✔
3589
    FLXSyncTestHarness::ServerSchema server_schema{schema, {}, {role}};
2✔
3590
    FLXSyncTestHarness harness("asymmetric_sync", server_schema);
2✔
3591

3592
    auto error_received_pf = util::make_promise_future<void>();
2✔
3593
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3594
    config.sync_config->on_sync_client_event_hook =
2✔
3595
        [promise = util::CopyablePromiseHolder(std::move(error_received_pf.promise))](
2✔
3596
            std::weak_ptr<SyncSession> weak_session, const SyncClientHookData& data) mutable {
30✔
3597
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
30✔
3598
                return SyncClientHookAction::NoAction;
26✔
3599
            }
26✔
3600
            auto session = weak_session.lock();
4✔
3601
            REQUIRE(session);
4!
3602

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

3605
            if (error_code == sync::ProtocolError::initial_sync_not_completed) {
4✔
3606
                return SyncClientHookAction::NoAction;
2✔
3607
            }
2✔
3608

3609
            REQUIRE(error_code == sync::ProtocolError::write_not_allowed);
2!
3610
            REQUIRE_FALSE(data.error_info->compensating_write_server_version.has_value());
2!
3611
            REQUIRE_FALSE(data.error_info->compensating_writes.empty());
2!
3612
            promise.get_promise().emplace_value();
2✔
3613

3614
            return SyncClientHookAction::EarlyReturn;
2✔
3615
        };
2✔
3616

3617
    auto realm = Realm::get_shared_realm(config);
2✔
3618

3619
    // Create an asymmetric object and upload it to the server.
3620
    {
2✔
3621
        realm->begin_transaction();
2✔
3622
        CppContext c(realm);
2✔
3623
        Object::create(c, realm, "Asymmetric",
2✔
3624
                       std::any(AnyDict{{"_id", ObjectId::gen()}, {"embedded_obj", AnyDict{{"value", "foo"s}}}}));
2✔
3625
        realm->commit_transaction();
2✔
3626
    }
2✔
3627

3628
    error_received_pf.future.get();
2✔
3629
    realm->close();
2✔
3630
}
2✔
3631

3632
TEST_CASE("flx: send client error", "[sync][flx][baas]") {
2✔
3633
    FLXSyncTestHarness harness("flx_client_error");
2✔
3634

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

3640
    auto [error_promise, error_future] = util::make_promise_future<SyncError>();
2✔
3641
    auto error_count = 0;
2✔
3642
    auto err_handler = [promise = util::CopyablePromiseHolder(std::move(error_promise)),
2✔
3643
                        &error_count](std::shared_ptr<SyncSession>, SyncError err) mutable {
4✔
3644
        ++error_count;
4✔
3645
        if (error_count == 1) {
4✔
3646
            // Bad changeset detected by the client.
3647
            CHECK(err.status == ErrorCodes::BadChangeset);
2!
3648
        }
2✔
3649
        else if (error_count == 2) {
2✔
3650
            // Server asking for a client reset.
3651
            CHECK(err.status == ErrorCodes::SyncClientResetRequired);
2!
3652
            CHECK(err.is_client_reset_requested());
2!
3653
            promise.get_promise().emplace_value(std::move(err));
2✔
3654
        }
2✔
3655
    };
4✔
3656

3657
    config.sync_config->error_handler = err_handler;
2✔
3658
    auto realm = Realm::get_shared_realm(config);
2✔
3659
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
3660
    auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3661
    new_query.insert_or_assign(Query(table));
2✔
3662
    new_query.commit();
2✔
3663

3664
    auto err = error_future.get();
2✔
3665
    CHECK(error_count == 2);
2!
3666
}
2✔
3667

3668
TEST_CASE("flx: bootstraps contain all changes", "[sync][flx][bootstrap][baas]") {
6✔
3669
    FLXSyncTestHarness harness("bootstrap_full_sync");
6✔
3670

3671
    auto setup_subs = [](SharedRealm& realm) {
12✔
3672
        auto table = realm->read_group().get_table("class_TopLevel");
12✔
3673
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
12✔
3674
        new_query.clear();
12✔
3675
        auto col = table->get_column_key("queryable_str_field");
12✔
3676
        new_query.insert_or_assign(Query(table).equal(col, StringData("bar")).Or().equal(col, StringData("bizz")));
12✔
3677
        return new_query.commit();
12✔
3678
    };
12✔
3679

3680
    auto bar_obj_id = ObjectId::gen();
6✔
3681
    auto bizz_obj_id = ObjectId::gen();
6✔
3682
    auto setup_and_poison_cache = [&] {
6✔
3683
        harness.load_initial_data([&](SharedRealm realm) {
6✔
3684
            CppContext c(realm);
6✔
3685
            Object::create(c, realm, "TopLevel",
6✔
3686
                           std::any(AnyDict{{"_id", bar_obj_id},
6✔
3687
                                            {"queryable_str_field", std::string{"bar"}},
6✔
3688
                                            {"queryable_int_field", static_cast<int64_t>(10)},
6✔
3689
                                            {"non_queryable_field", std::string{"non queryable 2"}}}));
6✔
3690
        });
6✔
3691

3692
        harness.do_with_new_realm([&](SharedRealm realm) {
6✔
3693
            // first set a subscription to force the creation/caching of a broker snapshot on the server.
3694
            setup_subs(realm).get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
6✔
3695
            wait_for_advance(*realm);
6✔
3696
            auto table = realm->read_group().get_table("class_TopLevel");
6✔
3697
            REQUIRE(table->find_primary_key(bar_obj_id));
6!
3698

3699
            // Then create an object that won't be in the cached snapshot - this is the object that if we didn't
3700
            // wait for a MARK message to come back, we'd miss it in our results.
3701
            CppContext c(realm);
6✔
3702
            realm->begin_transaction();
6✔
3703
            Object::create(c, realm, "TopLevel",
6✔
3704
                           std::any(AnyDict{{"_id", bizz_obj_id},
6✔
3705
                                            {"queryable_str_field", std::string{"bizz"}},
6✔
3706
                                            {"queryable_int_field", static_cast<int64_t>(15)},
6✔
3707
                                            {"non_queryable_field", std::string{"non queryable 3"}}}));
6✔
3708
            realm->commit_transaction();
6✔
3709
            wait_for_upload(*realm);
6✔
3710
        });
6✔
3711
    };
6✔
3712

3713
    SECTION("regular subscription change") {
6✔
3714
        SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3715
        std::atomic<bool> saw_truncated_bootstrap{false};
2✔
3716
        triggered_config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_sess,
2✔
3717
                                                                      const SyncClientHookData& data) {
44✔
3718
            auto sess = weak_sess.lock();
44✔
3719
            if (!sess || data.event != SyncClientHookEvent::BootstrapProcessed || data.query_version != 1) {
44✔
3720
                return SyncClientHookAction::NoAction;
42✔
3721
            }
42✔
3722

3723
            auto latest_subs = sess->get_flx_subscription_store()->get_latest();
2✔
3724
            REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3725
            REQUIRE(data.num_changesets == 1);
2!
3726
            auto db = SyncSession::OnlyForTesting::get_db(*sess);
2✔
3727
            auto read_tr = db->start_read();
2✔
3728
            auto table = read_tr->get_table("class_TopLevel");
2✔
3729
            REQUIRE(table->find_primary_key(bar_obj_id));
2!
3730
            REQUIRE_FALSE(table->find_primary_key(bizz_obj_id));
2!
3731
            saw_truncated_bootstrap.store(true);
2✔
3732

3733
            return SyncClientHookAction::NoAction;
2✔
3734
        };
2✔
3735
        auto problem_realm = Realm::get_shared_realm(triggered_config);
2✔
3736

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

3743
        nlohmann::json command_request = {
2✔
3744
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
3745
        };
2✔
3746
        auto resp_body =
2✔
3747
            SyncSession::OnlyForTesting::send_test_command(*problem_realm->sync_session(), command_request.dump())
2✔
3748
                .get();
2✔
3749
        REQUIRE(resp_body == "{}");
2!
3750

3751
        // Put some data into the server, this will be the data that will be in the broker cache.
3752
        setup_and_poison_cache();
2✔
3753

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

3760
        REQUIRE(saw_truncated_bootstrap.load());
2!
3761
        auto table = problem_realm->read_group().get_table("class_TopLevel");
2✔
3762
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3763
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3764
    }
2✔
3765

3766
// TODO: remote-baas: This test fails intermittently with Windows remote baas server - to be fixed in RCORE-1674
3767
#ifndef _WIN32
6✔
3768
    SECTION("disconnect between bootstrap and mark") {
6✔
3769
        SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3770
        auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
3771
        triggered_config.sync_config->on_sync_client_event_hook =
2✔
3772
            [promise = util::CopyablePromiseHolder(std::move(interrupted_promise)), &bizz_obj_id,
2✔
3773
             &bar_obj_id](std::weak_ptr<SyncSession> weak_sess, const SyncClientHookData& data) mutable {
52✔
3774
                auto sess = weak_sess.lock();
52✔
3775
                if (!sess || data.event != SyncClientHookEvent::BootstrapProcessed || data.query_version != 1) {
52✔
3776
                    return SyncClientHookAction::NoAction;
50✔
3777
                }
50✔
3778

3779
                auto latest_subs = sess->get_flx_subscription_store()->get_latest();
2✔
3780
                REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3781
                REQUIRE(data.num_changesets == 1);
2!
3782
                auto db = SyncSession::OnlyForTesting::get_db(*sess);
2✔
3783
                auto read_tr = db->start_read();
2✔
3784
                auto table = read_tr->get_table("class_TopLevel");
2✔
3785
                REQUIRE(table->find_primary_key(bar_obj_id));
2!
3786
                REQUIRE_FALSE(table->find_primary_key(bizz_obj_id));
2!
3787

3788
                sess->pause();
2✔
3789
                promise.get_promise().emplace_value();
2✔
3790
                return SyncClientHookAction::NoAction;
2✔
3791
            };
2✔
3792
        auto problem_realm = Realm::get_shared_realm(triggered_config);
2✔
3793

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

3800
        nlohmann::json command_request = {
2✔
3801
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
3802
        };
2✔
3803
        auto resp_body =
2✔
3804
            SyncSession::OnlyForTesting::send_test_command(*problem_realm->sync_session(), command_request.dump())
2✔
3805
                .get();
2✔
3806
        REQUIRE(resp_body == "{}");
2!
3807

3808
        // Put some data into the server, this will be the data that will be in the broker cache.
3809
        setup_and_poison_cache();
2✔
3810

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

3817
        interrupted.get();
2✔
3818
        problem_realm->sync_session()->shutdown_and_wait();
2✔
3819
        REQUIRE(sub_complete_future.is_ready());
2!
3820
        sub_set.refresh();
2✔
3821
        REQUIRE(sub_set.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3822

3823
        sub_complete_future = sub_set.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
3824
        problem_realm->sync_session()->resume();
2✔
3825
        sub_complete_future.get();
2✔
3826
        wait_for_advance(*problem_realm);
2✔
3827

3828
        sub_set.refresh();
2✔
3829
        REQUIRE(sub_set.state() == sync::SubscriptionSet::State::Complete);
2!
3830
        auto table = problem_realm->read_group().get_table("class_TopLevel");
2✔
3831
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3832
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3833
    }
2✔
3834
#endif
6✔
3835
    SECTION("error/suspend between bootstrap and mark") {
6✔
3836
        SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3837
        triggered_config.sync_config->on_sync_client_event_hook =
2✔
3838
            [&bizz_obj_id, &bar_obj_id](std::weak_ptr<SyncSession> weak_sess, const SyncClientHookData& data) {
52✔
3839
                auto sess = weak_sess.lock();
52✔
3840
                if (!sess || data.event != SyncClientHookEvent::BootstrapProcessed || data.query_version != 1) {
52✔
3841
                    return SyncClientHookAction::NoAction;
50✔
3842
                }
50✔
3843

3844
                auto latest_subs = sess->get_flx_subscription_store()->get_latest();
2✔
3845
                REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3846
                REQUIRE(data.num_changesets == 1);
2!
3847
                auto db = SyncSession::OnlyForTesting::get_db(*sess);
2✔
3848
                auto read_tr = db->start_read();
2✔
3849
                auto table = read_tr->get_table("class_TopLevel");
2✔
3850
                REQUIRE(table->find_primary_key(bar_obj_id));
2!
3851
                REQUIRE_FALSE(table->find_primary_key(bizz_obj_id));
2!
3852

3853
                return SyncClientHookAction::TriggerReconnect;
2✔
3854
            };
2✔
3855
        auto problem_realm = Realm::get_shared_realm(triggered_config);
2✔
3856

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

3863
        nlohmann::json command_request = {
2✔
3864
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
3865
        };
2✔
3866
        auto resp_body =
2✔
3867
            SyncSession::OnlyForTesting::send_test_command(*problem_realm->sync_session(), command_request.dump())
2✔
3868
                .get();
2✔
3869
        REQUIRE(resp_body == "{}");
2!
3870

3871
        // Put some data into the server, this will be the data that will be in the broker cache.
3872
        setup_and_poison_cache();
2✔
3873

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

3880
        sub_complete_future.get();
2✔
3881
        wait_for_advance(*problem_realm);
2✔
3882

3883
        sub_set.refresh();
2✔
3884
        REQUIRE(sub_set.state() == sync::SubscriptionSet::State::Complete);
2!
3885
        auto table = problem_realm->read_group().get_table("class_TopLevel");
2✔
3886
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3887
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3888
    }
2✔
3889
}
6✔
3890

3891
TEST_CASE("flx: convert flx sync realm to bundled realm", "[app][flx][baas]") {
12✔
3892
    static auto foo_obj_id = ObjectId::gen();
12✔
3893
    static auto bar_obj_id = ObjectId::gen();
12✔
3894
    static auto bizz_obj_id = ObjectId::gen();
12✔
3895
    static std::optional<FLXSyncTestHarness> harness;
12✔
3896
    if (!harness) {
12✔
3897
        harness.emplace("bundled_flx_realms");
2✔
3898
        harness->load_initial_data([&](SharedRealm realm) {
2✔
3899
            CppContext c(realm);
2✔
3900
            Object::create(c, realm, "TopLevel",
2✔
3901
                           std::any(AnyDict{{"_id", foo_obj_id},
2✔
3902
                                            {"queryable_str_field", "foo"s},
2✔
3903
                                            {"queryable_int_field", static_cast<int64_t>(5)},
2✔
3904
                                            {"non_queryable_field", "non queryable 1"s}}));
2✔
3905
            Object::create(c, realm, "TopLevel",
2✔
3906
                           std::any(AnyDict{{"_id", bar_obj_id},
2✔
3907
                                            {"queryable_str_field", "bar"s},
2✔
3908
                                            {"queryable_int_field", static_cast<int64_t>(10)},
2✔
3909
                                            {"non_queryable_field", "non queryable 2"s}}));
2✔
3910
        });
2✔
3911
    }
2✔
3912

3913
    SECTION("flx to flx (should succeed)") {
12✔
3914
        create_user_and_log_in(harness->app());
2✔
3915
        SyncTestFile target_config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
3916
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3917
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
3918
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3919
            mut_subs.insert_or_assign(Query(table).greater(table->get_column_key("queryable_int_field"), 5));
2✔
3920
            auto subs = std::move(mut_subs).commit();
2✔
3921

3922
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
3923
            wait_for_advance(*realm);
2✔
3924

3925
            realm->convert(target_config);
2✔
3926
        });
2✔
3927

3928
        auto target_realm = Realm::get_shared_realm(target_config);
2✔
3929

3930
        target_realm->begin_transaction();
2✔
3931
        CppContext c(target_realm);
2✔
3932
        Object::create(c, target_realm, "TopLevel",
2✔
3933
                       std::any(AnyDict{{"_id", bizz_obj_id},
2✔
3934
                                        {"queryable_str_field", "bizz"s},
2✔
3935
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
3936
                                        {"non_queryable_field", "non queryable 3"s}}));
2✔
3937
        target_realm->commit_transaction();
2✔
3938

3939
        wait_for_upload(*target_realm);
2✔
3940
        wait_for_download(*target_realm);
2✔
3941

3942
        auto latest_subs = target_realm->get_active_subscription_set();
2✔
3943
        auto table = target_realm->read_group().get_table("class_TopLevel");
2✔
3944
        REQUIRE(latest_subs.size() == 1);
2!
3945
        REQUIRE(latest_subs.at(0).object_class_name == "TopLevel");
2!
3946
        REQUIRE(latest_subs.at(0).query_string ==
2!
3947
                Query(table).greater(table->get_column_key("queryable_int_field"), 5).get_description());
2✔
3948

3949
        REQUIRE(table->size() == 2);
2!
3950
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3951
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3952
        REQUIRE_FALSE(table->find_primary_key(foo_obj_id));
2!
3953
    }
2✔
3954

3955
    SECTION("flx to local (should succeed)") {
12✔
3956
        TestFile target_config;
2✔
3957

3958
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3959
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
3960
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3961
            mut_subs.insert_or_assign(Query(table).greater(table->get_column_key("queryable_int_field"), 5));
2✔
3962
            auto subs = std::move(mut_subs).commit();
2✔
3963

3964
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
3965
            wait_for_advance(*realm);
2✔
3966

3967
            target_config.schema = realm->schema();
2✔
3968
            target_config.schema_version = realm->schema_version();
2✔
3969
            realm->convert(target_config);
2✔
3970
        });
2✔
3971

3972
        auto target_realm = Realm::get_shared_realm(target_config);
2✔
3973
        REQUIRE_THROWS(target_realm->get_active_subscription_set());
2✔
3974

3975
        auto table = target_realm->read_group().get_table("class_TopLevel");
2✔
3976
        REQUIRE(table->size() == 2);
2!
3977
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3978
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3979
        REQUIRE_FALSE(table->find_primary_key(foo_obj_id));
2!
3980
    }
2✔
3981

3982
    SECTION("flx to pbs (should fail to convert)") {
12✔
3983
        create_user_and_log_in(harness->app());
2✔
3984
        SyncTestFile target_config(harness->app()->current_user(), "12345"s, harness->schema());
2✔
3985
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3986
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
3987
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3988
            mut_subs.insert_or_assign(Query(table).greater(table->get_column_key("queryable_int_field"), 5));
2✔
3989
            auto subs = std::move(mut_subs).commit();
2✔
3990

3991
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
3992
            wait_for_advance(*realm);
2✔
3993

3994
            REQUIRE_THROWS(realm->convert(target_config));
2✔
3995
        });
2✔
3996
    }
2✔
3997

3998
    SECTION("pbs to flx (should fail to convert)") {
12✔
3999
        create_user_and_log_in(harness->app());
2✔
4000
        SyncTestFile target_config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
4001

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

4004
        TestAppSession pbs_app_session(create_app(pbs_app_config));
2✔
4005
        SyncTestFile source_config(pbs_app_session.app()->current_user(), "54321"s, pbs_app_config.schema);
2✔
4006
        auto realm = Realm::get_shared_realm(source_config);
2✔
4007

4008
        realm->begin_transaction();
2✔
4009
        CppContext c(realm);
2✔
4010
        Object::create(c, realm, "TopLevel",
2✔
4011
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
4012
                                        {"queryable_str_field", "foo"s},
2✔
4013
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4014
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
4015
        realm->commit_transaction();
2✔
4016

4017
        REQUIRE_THROWS(realm->convert(target_config));
2✔
4018
    }
2✔
4019

4020
    SECTION("local to flx (should fail to convert)") {
12✔
4021
        TestFile source_config;
2✔
4022
        source_config.schema = harness->schema();
2✔
4023
        source_config.schema_version = 1;
2✔
4024

4025
        auto realm = Realm::get_shared_realm(source_config);
2✔
4026
        auto foo_obj_id = ObjectId::gen();
2✔
4027

4028
        realm->begin_transaction();
2✔
4029
        CppContext c(realm);
2✔
4030
        Object::create(c, realm, "TopLevel",
2✔
4031
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
4032
                                        {"queryable_str_field", "foo"s},
2✔
4033
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4034
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
4035
        realm->commit_transaction();
2✔
4036

4037
        create_user_and_log_in(harness->app());
2✔
4038
        SyncTestFile target_config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
4039

4040
        REQUIRE_THROWS(realm->convert(target_config));
2✔
4041
    }
2✔
4042

4043
    // Add new sections before this
4044
    SECTION("teardown") {
12✔
4045
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
4046
        harness.reset();
2✔
4047
    }
2✔
4048
}
12✔
4049

4050
TEST_CASE("flx: compensating write errors get re-sent across sessions", "[sync][flx][compensating write][baas]") {
2✔
4051
    AppCreateConfig::ServiceRole role{"compensating_write_perms"};
2✔
4052
    role.document_filters.write = {
2✔
4053
        {"queryable_str_field", nlohmann::json{{"$in", nlohmann::json::array({"foo", "bar"})}}}};
2✔
4054

4055
    FLXSyncTestHarness::ServerSchema server_schema{
2✔
4056
        g_simple_embedded_obj_schema, {"queryable_str_field", "queryable_int_field"}, {role}};
2✔
4057
    FLXSyncTestHarness::Config harness_config("flx_bad_query", server_schema);
2✔
4058
    harness_config.reconnect_mode = ReconnectMode::testing;
2✔
4059
    FLXSyncTestHarness harness(std::move(harness_config));
2✔
4060

4061
    auto test_obj_id_1 = ObjectId::gen();
2✔
4062
    auto test_obj_id_2 = ObjectId::gen();
2✔
4063

4064
    create_user_and_log_in(harness.app());
2✔
4065
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4066

4067
    {
2✔
4068
        auto error_received_pf = util::make_promise_future<void>();
2✔
4069
        config.sync_config->on_sync_client_event_hook =
2✔
4070
            [promise = util::CopyablePromiseHolder(std::move(error_received_pf.promise))](
2✔
4071
                std::weak_ptr<SyncSession> weak_session, const SyncClientHookData& data) mutable {
46✔
4072
                if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
46✔
4073
                    return SyncClientHookAction::NoAction;
44✔
4074
                }
44✔
4075
                auto session = weak_session.lock();
2✔
4076
                REQUIRE(session);
2!
4077

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

4080
                if (error_code == sync::ProtocolError::initial_sync_not_completed) {
2✔
4081
                    return SyncClientHookAction::NoAction;
×
4082
                }
×
4083

4084
                REQUIRE(error_code == sync::ProtocolError::compensating_write);
2!
4085
                REQUIRE_FALSE(data.error_info->compensating_writes.empty());
2!
4086
                promise.get_promise().emplace_value();
2✔
4087

4088
                return SyncClientHookAction::TriggerReconnect;
2✔
4089
            };
2✔
4090

4091
        auto realm = Realm::get_shared_realm(config);
2✔
4092
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
4093
        auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
4094
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4095
        new_query.insert_or_assign(Query(table).equal(queryable_str_field, "bizz"));
2✔
4096
        std::move(new_query).commit();
2✔
4097

4098
        wait_for_upload(*realm);
2✔
4099
        wait_for_download(*realm);
2✔
4100

4101
        CppContext c(realm);
2✔
4102
        realm->begin_transaction();
2✔
4103
        Object::create(c, realm, "TopLevel",
2✔
4104
                       util::Any(AnyDict{
2✔
4105
                           {"_id", test_obj_id_1},
2✔
4106
                           {"queryable_str_field", std::string{"foo"}},
2✔
4107
                       }));
2✔
4108
        realm->commit_transaction();
2✔
4109

4110
        realm->begin_transaction();
2✔
4111
        Object::create(c, realm, "TopLevel",
2✔
4112
                       util::Any(AnyDict{
2✔
4113
                           {"_id", test_obj_id_2},
2✔
4114
                           {"queryable_str_field", std::string{"baz"}},
2✔
4115
                       }));
2✔
4116
        realm->commit_transaction();
2✔
4117

4118
        error_received_pf.future.get();
2✔
4119
        realm->sync_session()->shutdown_and_wait();
2✔
4120
        config.sync_config->on_sync_client_event_hook = {};
2✔
4121
    }
2✔
4122

4123
    _impl::RealmCoordinator::clear_all_caches();
2✔
4124

4125
    std::mutex errors_mutex;
2✔
4126
    std::condition_variable new_compensating_write;
2✔
4127
    std::vector<std::pair<ObjectId, sync::version_type>> error_to_download_version;
2✔
4128
    std::vector<sync::CompensatingWriteErrorInfo> compensating_writes;
2✔
4129
    sync::version_type download_version;
2✔
4130

4131
    config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
2✔
4132
                                                        const SyncClientHookData& data) mutable {
27✔
4133
        auto session = weak_session.lock();
27✔
4134
        if (!session) {
27✔
4135
            return SyncClientHookAction::NoAction;
×
4136
        }
×
4137

4138
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
27✔
4139
            if (data.event == SyncClientHookEvent::DownloadMessageReceived) {
23✔
4140
                download_version = data.progress.download.server_version;
5✔
4141
            }
5✔
4142

4143
            return SyncClientHookAction::NoAction;
23✔
4144
        }
23✔
4145

4146
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
4147
        REQUIRE(error_code == sync::ProtocolError::compensating_write);
4!
4148
        REQUIRE(!data.error_info->compensating_writes.empty());
4!
4149
        std::lock_guard<std::mutex> lk(errors_mutex);
4✔
4150
        for (const auto& compensating_write : data.error_info->compensating_writes) {
4✔
4151
            error_to_download_version.emplace_back(compensating_write.primary_key.get_object_id(),
4✔
4152
                                                   *data.error_info->compensating_write_server_version);
4✔
4153
        }
4✔
4154

4155
        return SyncClientHookAction::NoAction;
4✔
4156
    };
4✔
4157

4158
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
4✔
4159
        std::unique_lock<std::mutex> lk(errors_mutex);
4✔
4160
        REQUIRE(error.status == ErrorCodes::SyncCompensatingWrite);
4!
4161
        for (const auto& compensating_write : error.compensating_writes_info) {
4✔
4162
            auto tracked_error = std::find_if(error_to_download_version.begin(), error_to_download_version.end(),
4✔
4163
                                              [&](const auto& pair) {
6✔
4164
                                                  return pair.first == compensating_write.primary_key.get_object_id();
6✔
4165
                                              });
6✔
4166
            REQUIRE(tracked_error != error_to_download_version.end());
4!
4167
            CHECK(tracked_error->second <= download_version);
4!
4168
            compensating_writes.push_back(compensating_write);
4✔
4169
        }
4✔
4170
        new_compensating_write.notify_one();
4✔
4171
    };
4✔
4172

4173
    auto realm = Realm::get_shared_realm(config);
2✔
4174

4175
    wait_for_upload(*realm);
2✔
4176
    wait_for_download(*realm);
2✔
4177

4178
    std::unique_lock<std::mutex> lk(errors_mutex);
2✔
4179
    new_compensating_write.wait_for(lk, std::chrono::seconds(30), [&] {
2✔
4180
        return compensating_writes.size() == 2;
2✔
4181
    });
2✔
4182

4183
    REQUIRE(compensating_writes.size() == 2);
2!
4184
    auto& write_info = compensating_writes[0];
2✔
4185
    CHECK(write_info.primary_key.is_type(type_ObjectId));
2!
4186
    CHECK(write_info.primary_key.get_object_id() == test_obj_id_1);
2!
4187
    CHECK(write_info.object_name == "TopLevel");
2!
4188
    CHECK_THAT(write_info.reason, Catch::Matchers::ContainsSubstring("object is outside of the current query view"));
2✔
4189

4190
    write_info = compensating_writes[1];
2✔
4191
    REQUIRE(write_info.primary_key.is_type(type_ObjectId));
2!
4192
    REQUIRE(write_info.primary_key.get_object_id() == test_obj_id_2);
2!
4193
    REQUIRE(write_info.object_name == "TopLevel");
2!
4194
    REQUIRE(write_info.reason ==
2!
4195
            util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed", test_obj_id_2));
2✔
4196
    auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
4197
    REQUIRE(top_level_table->is_empty());
2!
4198
}
2✔
4199

4200
TEST_CASE("flx: compensating write errors are not duplicated", "[sync][flx][compensating write][baas]") {
2✔
4201
    FLXSyncTestHarness harness("flx_compensating_writes");
2✔
4202
    auto config = harness.make_test_file();
2✔
4203

4204
    auto test_obj_id_1 = ObjectId::gen();
2✔
4205
    auto test_obj_id_2 = ObjectId::gen();
2✔
4206

4207
    enum class TestState { Start, FirstError, SecondError, Resume, ThirdError, FourthError };
2✔
4208
    TestingStateMachine<TestState> state(TestState::Start);
2✔
4209

4210
    std::mutex errors_mutex;
2✔
4211
    std::vector<std::pair<ObjectId, sync::version_type>> error_to_download_version;
2✔
4212
    std::vector<sync::CompensatingWriteErrorInfo> compensating_writes;
2✔
4213
    sync::version_type download_version;
2✔
4214

4215
    config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
2✔
4216
                                                        const SyncClientHookData& data) {
83✔
4217
        if (auto session = weak_session.lock(); !session) {
83✔
UNCOV
4218
            return SyncClientHookAction::NoAction;
×
UNCOV
4219
        }
×
4220
        SyncClientHookAction action = SyncClientHookAction::NoAction;
83✔
4221
        state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
83✔
4222
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
83✔
4223
                // Before the session is resumed, ignore the download messages received
4224
                // to undo the out-of-view writes.
4225
                if (data.event == SyncClientHookEvent::DownloadMessageReceived &&
73✔
4226
                    (cur_state == TestState::FirstError || cur_state == TestState::SecondError)) {
73✔
4227
                    action = SyncClientHookAction::EarlyReturn;
4✔
4228
                }
4✔
4229
                else if (data.event == SyncClientHookEvent::DownloadMessageReceived &&
69✔
4230
                         (cur_state == TestState::Resume || cur_state == TestState::ThirdError)) {
69✔
4231
                    download_version = data.progress.download.server_version;
5✔
4232
                }
5✔
4233
                else if (data.event == SyncClientHookEvent::BindMessageSent && cur_state == TestState::SecondError) {
64✔
4234
                    return TestState::Resume;
2✔
4235
                }
2✔
4236
                return std::nullopt;
71✔
4237
            }
73✔
4238

4239
            auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
10✔
4240
            if (error_code == sync::ProtocolError::initial_sync_not_completed) {
10✔
4241
                return std::nullopt;
2✔
4242
            }
2✔
4243

4244
            REQUIRE(error_code == sync::ProtocolError::compensating_write);
8!
4245
            REQUIRE_FALSE(data.error_info->compensating_writes.empty());
8!
4246

4247
            if (cur_state == TestState::Start) {
8✔
4248
                return TestState::FirstError;
2✔
4249
            }
2✔
4250
            else if (cur_state == TestState::FirstError) {
6✔
4251
                // Return early so the second compensating write error is not saved
4252
                // by the sync client.
4253
                // This is so server versions received to undo the out-of-view writes are
4254
                // [x, x, y] instead of [x, y, x, y] (server versions don't increase
4255
                // monotonically as the client expects).
4256
                action = SyncClientHookAction::EarlyReturn;
2✔
4257
                return TestState::SecondError;
2✔
4258
            }
2✔
4259
            // Save third and fourth compensating write errors after resume.
4260
            std::lock_guard<std::mutex> lk(errors_mutex);
4✔
4261
            for (const auto& compensating_write : data.error_info->compensating_writes) {
4✔
4262
                error_to_download_version.emplace_back(compensating_write.primary_key.get_object_id(),
4✔
4263
                                                       *data.error_info->compensating_write_server_version);
4✔
4264
            }
4✔
4265
            return std::nullopt;
4✔
4266
        });
8✔
4267
        return action;
83✔
4268
    };
83✔
4269

4270
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
4✔
4271
        std::unique_lock<std::mutex> lk(errors_mutex);
4✔
4272
        REQUIRE(error.status == ErrorCodes::SyncCompensatingWrite);
4!
4273
        for (const auto& compensating_write : error.compensating_writes_info) {
4✔
4274
            auto tracked_error = std::find_if(error_to_download_version.begin(), error_to_download_version.end(),
4✔
4275
                                              [&](const auto& pair) {
6✔
4276
                                                  return pair.first == compensating_write.primary_key.get_object_id();
6✔
4277
                                              });
6✔
4278
            REQUIRE(tracked_error != error_to_download_version.end());
4!
4279
            CHECK(tracked_error->second <= download_version);
4!
4280
            compensating_writes.push_back(compensating_write);
4✔
4281
        }
4✔
4282
        lk.unlock();
4✔
4283

4284
        state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
4✔
4285
            if (cur_state == TestState::Resume) {
4✔
4286
                return TestState::ThirdError;
2✔
4287
            }
2✔
4288
            else if (cur_state == TestState::ThirdError) {
2✔
4289
                return TestState::FourthError;
2✔
4290
            }
2✔
4291
            return std::nullopt;
×
4292
        });
4✔
4293
    };
4✔
4294

4295
    auto realm = Realm::get_shared_realm(config);
2✔
4296
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
4297
    auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
4298
    auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4299
    new_query.insert_or_assign(Query(table).equal(queryable_str_field, "bizz"));
2✔
4300
    std::move(new_query).commit();
2✔
4301

4302
    wait_for_upload(*realm);
2✔
4303
    wait_for_download(*realm);
2✔
4304

4305
    CppContext c(realm);
2✔
4306
    realm->begin_transaction();
2✔
4307
    Object::create(c, realm, "TopLevel",
2✔
4308
                   util::Any(AnyDict{
2✔
4309
                       {"_id", test_obj_id_1},
2✔
4310
                       {"queryable_str_field", std::string{"foo"}},
2✔
4311
                   }));
2✔
4312
    realm->commit_transaction();
2✔
4313

4314
    realm->begin_transaction();
2✔
4315
    Object::create(c, realm, "TopLevel",
2✔
4316
                   util::Any(AnyDict{
2✔
4317
                       {"_id", test_obj_id_2},
2✔
4318
                       {"queryable_str_field", std::string{"baz"}},
2✔
4319
                   }));
2✔
4320
    realm->commit_transaction();
2✔
4321
    state.wait_for(TestState::SecondError);
2✔
4322

4323
    nlohmann::json error_body = {
2✔
4324
        {"tryAgain", true},           {"message", "fake error"},
2✔
4325
        {"shouldClientReset", false}, {"isRecoveryModeDisabled", false},
2✔
4326
        {"action", "Transient"},
2✔
4327
    };
2✔
4328
    nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
4329
                                   {"args", nlohmann::json{{"errorCode", 229}, {"errorBody", error_body}}}};
2✔
4330

4331
    // Trigger a retryable transient error to resume the session.
4332
    auto test_cmd_res =
2✔
4333
        wait_for_future(SyncSession::OnlyForTesting::send_test_command(*realm->sync_session(), test_command.dump()))
2✔
4334
            .get();
2✔
4335
    CHECK(test_cmd_res == "{}");
2!
4336
    state.wait_for(TestState::FourthError);
2✔
4337

4338
    REQUIRE(compensating_writes.size() == 2);
2!
4339
    auto& write_info = compensating_writes[0];
2✔
4340
    CHECK(write_info.primary_key.is_type(type_ObjectId));
2!
4341
    CHECK(write_info.primary_key.get_object_id() == test_obj_id_1);
2!
4342
    CHECK(write_info.object_name == "TopLevel");
2!
4343
    CHECK(write_info.reason == util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed; object is "
2!
4344
                                            "outside of the current query view",
2✔
4345
                                            test_obj_id_1));
2✔
4346

4347
    write_info = compensating_writes[1];
2✔
4348
    CHECK(write_info.primary_key.is_type(type_ObjectId));
2!
4349
    CHECK(write_info.primary_key.get_object_id() == test_obj_id_2);
2!
4350
    CHECK(write_info.object_name == "TopLevel");
2!
4351
    CHECK(write_info.reason == util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed; object is "
2!
4352
                                            "outside of the current query view",
2✔
4353
                                            test_obj_id_2));
2✔
4354
    realm->refresh();
2✔
4355
    auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
4356
    CHECK(top_level_table->is_empty());
2!
4357
}
2✔
4358

4359
TEST_CASE("flx: bootstrap changesets are applied continuously", "[sync][flx][bootstrap][baas]") {
2✔
4360
    FLXSyncTestHarness harness("flx_bootstrap_ordering", {g_large_array_schema, {"queryable_int_field"}});
2✔
4361
    fill_large_array_schema(harness);
2✔
4362

4363
    std::unique_ptr<std::thread> th;
2✔
4364
    sync::version_type user_commit_version = UINT_FAST64_MAX;
2✔
4365
    sync::version_type bootstrap_version = UINT_FAST64_MAX;
2✔
4366
    SharedRealm realm;
2✔
4367
    std::condition_variable cv;
2✔
4368
    std::mutex mutex;
2✔
4369
    bool allow_to_commit = false;
2✔
4370

4371
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4372
    auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
4373
    auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
4374
    config.sync_config->on_sync_client_event_hook =
2✔
4375
        [promise = std::move(shared_promise), &th, &realm, &user_commit_version, &bootstrap_version, &cv, &mutex,
2✔
4376
         &allow_to_commit](std::weak_ptr<SyncSession> weak_session, const SyncClientHookData& data) {
80✔
4377
            if (data.query_version == 0) {
80✔
4378
                return SyncClientHookAction::NoAction;
24✔
4379
            }
24✔
4380
            if (data.event != SyncClientHookEvent::DownloadMessageIntegrated) {
56✔
4381
                return SyncClientHookAction::NoAction;
44✔
4382
            }
44✔
4383
            auto session = weak_session.lock();
12✔
4384
            if (!session) {
12✔
4385
                return SyncClientHookAction::NoAction;
×
4386
            }
×
4387
            if (data.batch_state != sync::DownloadBatchState::MoreToCome) {
12✔
4388
                // Read version after bootstrap is done.
4389
                auto db = TestHelper::get_db(realm);
2✔
4390
                ReadTransaction rt(db);
2✔
4391
                bootstrap_version = rt.get_version();
2✔
4392
                {
2✔
4393
                    std::lock_guard<std::mutex> lock(mutex);
2✔
4394
                    allow_to_commit = true;
2✔
4395
                }
2✔
4396
                cv.notify_one();
2✔
4397
                session->force_close();
2✔
4398
                promise->emplace_value();
2✔
4399
                return SyncClientHookAction::NoAction;
2✔
4400
            }
2✔
4401

4402
            if (th) {
10✔
4403
                return SyncClientHookAction::NoAction;
8✔
4404
            }
8✔
4405

4406
            auto func = [&] {
2✔
4407
                // Attempt to commit a local change after the first bootstrap batch was committed.
4408
                auto db = TestHelper::get_db(realm);
2✔
4409
                WriteTransaction wt(db);
2✔
4410
                TableRef table = wt.get_table("class_TopLevel");
2✔
4411
                table->create_object_with_primary_key(ObjectId::gen());
2✔
4412
                {
2✔
4413
                    std::unique_lock<std::mutex> lock(mutex);
2✔
4414
                    // Wait to commit until we read the final bootstrap version.
4415
                    cv.wait(lock, [&] {
2✔
4416
                        return allow_to_commit;
2✔
4417
                    });
2✔
4418
                }
2✔
4419
                user_commit_version = wt.commit();
2✔
4420
            };
2✔
4421
            th = std::make_unique<std::thread>(std::move(func));
2✔
4422

4423
            return SyncClientHookAction::NoAction;
2✔
4424
        };
10✔
4425

4426
    realm = Realm::get_shared_realm(config);
2✔
4427
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
4428
    Query query(table);
2✔
4429
    {
2✔
4430
        auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4431
        new_subs.insert_or_assign(query);
2✔
4432
        new_subs.commit();
2✔
4433
    }
2✔
4434
    interrupted.get();
2✔
4435
    th->join();
2✔
4436

4437
    // The user commit is the last one.
4438
    CHECK(user_commit_version == bootstrap_version + 1);
2!
4439
}
2✔
4440

4441
TEST_CASE("flx: open realm + register subscription callback while bootstrapping",
4442
          "[sync][flx][bootstrap][async open][baas]") {
14✔
4443
    FLXSyncTestHarness harness("flx_bootstrap_and_subscribe");
14✔
4444
    auto foo_obj_id = ObjectId::gen();
14✔
4445
    int64_t foo_obj_queryable_int = 5;
14✔
4446
    harness.load_initial_data([&](SharedRealm realm) {
14✔
4447
        CppContext c(realm);
14✔
4448
        Object::create(c, realm, "TopLevel",
14✔
4449
                       std::any(AnyDict{{"_id", foo_obj_id},
14✔
4450
                                        {"queryable_str_field", "foo"s},
14✔
4451
                                        {"queryable_int_field", foo_obj_queryable_int},
14✔
4452
                                        {"non_queryable_field", "created as initial data seed"s}}));
14✔
4453
    });
14✔
4454
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
14✔
4455

4456
    std::atomic<bool> subscription_invoked = false;
14✔
4457
    auto subscription_pf = util::make_promise_future<bool>();
14✔
4458
    // create a subscription to commit when realm is open for the first time or asked to rerun on open
4459
    auto init_subscription_callback_with_promise =
14✔
4460
        [&, promise_holder = util::CopyablePromiseHolder(std::move(subscription_pf.promise))](
14✔
4461
            std::shared_ptr<Realm> realm) mutable {
14✔
4462
            REQUIRE(realm);
8!
4463
            auto table = realm->read_group().get_table("class_TopLevel");
8✔
4464
            Query query(table);
8✔
4465
            auto subscription = realm->get_latest_subscription_set();
8✔
4466
            auto mutable_subscription = subscription.make_mutable_copy();
8✔
4467
            mutable_subscription.insert_or_assign(query);
8✔
4468
            auto promise = promise_holder.get_promise();
8✔
4469
            mutable_subscription.commit();
8✔
4470
            subscription_invoked = true;
8✔
4471
            promise.emplace_value(true);
8✔
4472
        };
8✔
4473
    // verify that the subscription has changed the database
4474
    auto verify_subscription = [](SharedRealm realm) {
14✔
4475
        REQUIRE(realm);
12!
4476
        auto table_ref = realm->read_group().get_table("class_TopLevel");
12✔
4477
        REQUIRE(table_ref);
12!
4478
        REQUIRE(table_ref->get_column_count() == 4);
12!
4479
        REQUIRE(table_ref->get_column_key("_id"));
12!
4480
        REQUIRE(table_ref->get_column_key("queryable_str_field"));
12!
4481
        REQUIRE(table_ref->get_column_key("queryable_int_field"));
12!
4482
        REQUIRE(table_ref->get_column_key("non_queryable_field"));
12!
4483
        REQUIRE(table_ref->size() == 1);
12!
4484
        auto str_col = table_ref->get_column_key("queryable_str_field");
12✔
4485
        REQUIRE(table_ref->get_object(0).get<String>(str_col) == "foo");
12!
4486
        return true;
12✔
4487
    };
12✔
4488

4489
    SECTION("Sync open") {
14✔
4490
        // sync open with subscription callback. Subscription will be run, since this is the first time that realm is
4491
        // opened
4492
        subscription_invoked = false;
2✔
4493
        config.sync_config->subscription_initializer = init_subscription_callback_with_promise;
2✔
4494
        auto realm = Realm::get_shared_realm(config);
2✔
4495
        REQUIRE(subscription_pf.future.get());
2!
4496
        auto sb = realm->get_latest_subscription_set();
2✔
4497
        auto future = sb.get_state_change_notification(realm::sync::SubscriptionSet::State::Complete);
2✔
4498
        auto state = future.get();
2✔
4499
        REQUIRE(state == realm::sync::SubscriptionSet::State::Complete);
2!
4500
        realm->refresh(); // refresh is needed otherwise table_ref->size() would be 0
2✔
4501
        REQUIRE(verify_subscription(realm));
2!
4502
    }
2✔
4503

4504
    SECTION("Sync Open + Async Open") {
14✔
4505
        {
2✔
4506
            subscription_invoked = false;
2✔
4507
            config.sync_config->subscription_initializer = init_subscription_callback_with_promise;
2✔
4508
            auto realm = Realm::get_shared_realm(config);
2✔
4509
            REQUIRE(subscription_pf.future.get());
2!
4510
            auto sb = realm->get_latest_subscription_set();
2✔
4511
            auto future = sb.get_state_change_notification(realm::sync::SubscriptionSet::State::Complete);
2✔
4512
            auto state = future.get();
2✔
4513
            REQUIRE(state == realm::sync::SubscriptionSet::State::Complete);
2!
4514
            realm->refresh(); // refresh is needed otherwise table_ref->size() would be 0
2✔
4515
            REQUIRE(verify_subscription(realm));
2!
4516
        }
2✔
4517
        {
2✔
4518
            auto subscription_pf_async = util::make_promise_future<bool>();
2✔
4519
            auto init_subscription_asyc_callback =
2✔
4520
                [promise_holder_async = util::CopyablePromiseHolder(std::move(subscription_pf_async.promise))](
2✔
4521
                    std::shared_ptr<Realm> realm) mutable {
2✔
4522
                    REQUIRE(realm);
2!
4523
                    auto table = realm->read_group().get_table("class_TopLevel");
2✔
4524
                    Query query(table);
2✔
4525
                    auto subscription = realm->get_latest_subscription_set();
2✔
4526
                    auto mutable_subscription = subscription.make_mutable_copy();
2✔
4527
                    mutable_subscription.insert_or_assign(query);
2✔
4528
                    auto promise = promise_holder_async.get_promise();
2✔
4529
                    mutable_subscription.commit();
2✔
4530
                    promise.emplace_value(true);
2✔
4531
                };
2✔
4532
            auto open_realm_pf = util::make_promise_future<bool>();
2✔
4533
            auto open_realm_completed_callback = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
2✔
4534
                bool result = false;
2✔
4535
                if (!err) {
2✔
4536
                    result =
2✔
4537
                        verify_subscription(Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
2✔
4538
                }
2✔
4539
                open_realm_pf.promise.emplace_value(result);
2✔
4540
            };
2✔
4541

4542
            config.sync_config->subscription_initializer = init_subscription_asyc_callback;
2✔
4543
            config.sync_config->rerun_init_subscription_on_open = true;
2✔
4544
            auto async_open = Realm::get_synchronized_realm(config);
2✔
4545
            async_open->start(open_realm_completed_callback);
2✔
4546
            REQUIRE(open_realm_pf.future.get());
2!
4547
            REQUIRE(subscription_pf_async.future.get());
2!
4548
            config.sync_config->rerun_init_subscription_on_open = false;
2✔
4549
            auto realm = Realm::get_shared_realm(config);
2✔
4550
            REQUIRE(realm->get_latest_subscription_set().version() == 2);
2!
4551
            REQUIRE(realm->get_active_subscription_set().version() == 2);
2!
4552
        }
2✔
4553
    }
2✔
4554

4555
    SECTION("Async open") {
14✔
4556
        SECTION("Initial async open with no rerun on open set") {
10✔
4557
            // subscription will be run since this is the first time we are opening the realm file.
4558
            auto open_realm_pf = util::make_promise_future<bool>();
4✔
4559
            auto open_realm_completed_callback = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
4✔
4560
                bool result = false;
4✔
4561
                if (!err) {
4✔
4562
                    result =
4✔
4563
                        verify_subscription(Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
4✔
4564
                }
4✔
4565
                open_realm_pf.promise.emplace_value(result);
4✔
4566
            };
4✔
4567

4568
            config.sync_config->subscription_initializer = init_subscription_callback_with_promise;
4✔
4569
            auto async_open = Realm::get_synchronized_realm(config);
4✔
4570
            async_open->start(open_realm_completed_callback);
4✔
4571
            REQUIRE(open_realm_pf.future.get());
4!
4572
            REQUIRE(subscription_pf.future.get());
4!
4573

4574
            SECTION("rerun on open = false. Subscription not run") {
4✔
4575
                subscription_invoked = false;
2✔
4576
                auto async_open = Realm::get_synchronized_realm(config);
2✔
4577
                auto open_realm_pf = util::make_promise_future<bool>();
2✔
4578
                auto open_realm_completed_callback = [&](ThreadSafeReference, std::exception_ptr e) mutable {
2✔
4579
                    // no need to verify if the subscription has changed the db, since it has not run as we test
4580
                    // below
4581
                    open_realm_pf.promise.emplace_value(!e);
2✔
4582
                };
2✔
4583
                async_open->start(open_realm_completed_callback);
2✔
4584
                REQUIRE(open_realm_pf.future.get());
2!
4585
                REQUIRE_FALSE(subscription_invoked.load());
2!
4586
            }
2✔
4587

4588
            SECTION("rerun on open = true. Subscription not run cause realm already opened once") {
4✔
4589
                subscription_invoked = false;
2✔
4590
                auto realm = Realm::get_shared_realm(config);
2✔
4591
                auto init_subscription = [&subscription_invoked](std::shared_ptr<Realm> realm) mutable {
2✔
4592
                    REQUIRE(realm);
×
4593
                    auto table = realm->read_group().get_table("class_TopLevel");
×
4594
                    Query query(table);
×
4595
                    auto subscription = realm->get_latest_subscription_set();
×
4596
                    auto mutable_subscription = subscription.make_mutable_copy();
×
4597
                    mutable_subscription.insert_or_assign(query);
×
4598
                    mutable_subscription.commit();
×
4599
                    subscription_invoked.store(true);
×
4600
                };
×
4601
                config.sync_config->rerun_init_subscription_on_open = true;
2✔
4602
                config.sync_config->subscription_initializer = init_subscription;
2✔
4603
                auto async_open = Realm::get_synchronized_realm(config);
2✔
4604
                auto open_realm_pf = util::make_promise_future<bool>();
2✔
4605
                auto open_realm_completed_callback = [&](ThreadSafeReference, std::exception_ptr e) mutable {
2✔
4606
                    // no need to verify if the subscription has changed the db, since it has not run as we test
4607
                    // below
4608
                    open_realm_pf.promise.emplace_value(!e);
2✔
4609
                };
2✔
4610
                async_open->start(open_realm_completed_callback);
2✔
4611
                REQUIRE(open_realm_pf.future.get());
2!
4612
                REQUIRE_FALSE(subscription_invoked.load());
2!
4613
                REQUIRE(realm->get_latest_subscription_set().version() == 1);
2!
4614
                REQUIRE(realm->get_active_subscription_set().version() == 1);
2!
4615
            }
2✔
4616
        }
4✔
4617

4618
        SECTION("rerun on open set for multiple async open tasks (subscription runs only once)") {
10✔
4619
            auto init_subscription = [](std::shared_ptr<Realm> realm) mutable {
8✔
4620
                REQUIRE(realm);
8!
4621
                auto table = realm->read_group().get_table("class_TopLevel");
8✔
4622
                Query query(table);
8✔
4623
                auto subscription = realm->get_latest_subscription_set();
8✔
4624
                auto mutable_subscription = subscription.make_mutable_copy();
8✔
4625
                mutable_subscription.insert_or_assign(query);
8✔
4626
                mutable_subscription.commit();
8✔
4627
            };
8✔
4628

4629
            auto open_task1_pf = util::make_promise_future<SharedRealm>();
4✔
4630
            auto open_task2_pf = util::make_promise_future<SharedRealm>();
4✔
4631
            auto open_callback1 = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
4✔
4632
                REQUIRE_FALSE(err);
4!
4633
                open_task1_pf.promise.emplace_value(
4✔
4634
                    Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
4✔
4635
            };
4✔
4636
            auto open_callback2 = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
4✔
4637
                REQUIRE_FALSE(err);
4!
4638
                open_task2_pf.promise.emplace_value(
4✔
4639
                    Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
4✔
4640
            };
4✔
4641

4642
            config.sync_config->rerun_init_subscription_on_open = true;
4✔
4643
            config.sync_config->subscription_initializer = init_subscription;
4✔
4644

4645
            SECTION("Realm was already created, but we want to rerun on first open using multiple tasks") {
4✔
4646
                {
2✔
4647
                    subscription_invoked = false;
2✔
4648
                    auto realm = Realm::get_shared_realm(config);
2✔
4649
                    auto sb = realm->get_latest_subscription_set();
2✔
4650
                    auto future = sb.get_state_change_notification(realm::sync::SubscriptionSet::State::Complete);
2✔
4651
                    auto state = future.get();
2✔
4652
                    REQUIRE(state == realm::sync::SubscriptionSet::State::Complete);
2!
4653
                    realm->refresh(); // refresh is needed otherwise table_ref->size() would be 0
2✔
4654
                    REQUIRE(verify_subscription(realm));
2!
4655
                    REQUIRE(realm->get_latest_subscription_set().version() == 1);
2!
4656
                    REQUIRE(realm->get_active_subscription_set().version() == 1);
2!
4657
                }
2✔
4658

4659
                auto async_open_task1 = Realm::get_synchronized_realm(config);
2✔
4660
                auto async_open_task2 = Realm::get_synchronized_realm(config);
2✔
4661
                async_open_task1->start(open_callback1);
2✔
4662
                async_open_task2->start(open_callback2);
2✔
4663

4664
                auto realm1 = open_task1_pf.future.get();
2✔
4665
                auto realm2 = open_task2_pf.future.get();
2✔
4666

4667
                const auto version_expected = 2;
2✔
4668
                auto r1_latest = realm1->get_latest_subscription_set().version();
2✔
4669
                auto r1_active = realm1->get_active_subscription_set().version();
2✔
4670
                REQUIRE(realm2->get_latest_subscription_set().version() == r1_latest);
2!
4671
                REQUIRE(realm2->get_active_subscription_set().version() == r1_active);
2!
4672
                REQUIRE(r1_latest == version_expected);
2!
4673
                REQUIRE(r1_active == version_expected);
2!
4674
            }
2✔
4675
            SECTION("First time realm is created but opened via open async. Both tasks could run the subscription") {
4✔
4676
                auto async_open_task1 = Realm::get_synchronized_realm(config);
2✔
4677
                auto async_open_task2 = Realm::get_synchronized_realm(config);
2✔
4678
                async_open_task1->start(open_callback1);
2✔
4679
                async_open_task2->start(open_callback2);
2✔
4680
                auto realm1 = open_task1_pf.future.get();
2✔
4681
                auto realm2 = open_task2_pf.future.get();
2✔
4682

4683
                auto r1_latest = realm1->get_latest_subscription_set().version();
2✔
4684
                auto r1_active = realm1->get_active_subscription_set().version();
2✔
4685
                REQUIRE(realm2->get_latest_subscription_set().version() == r1_latest);
2!
4686
                REQUIRE(realm2->get_active_subscription_set().version() == r1_active);
2!
4687
                // the callback may be run twice, if task1 is the first task to open realm
4688
                // but it is scheduled after tasks2, which have opened realm later but
4689
                // by the time it runs, subscription version is equal to 0 (realm creation).
4690
                // This can only happen the first time that realm is created. All the other times
4691
                // the init_sb callback is guaranteed to run once.
4692
                REQUIRE(r1_latest >= 1);
2!
4693
                REQUIRE(r1_latest <= 2);
2!
4694
                REQUIRE(r1_active >= 1);
2!
4695
                REQUIRE(r1_active <= 2);
2!
4696
            }
2✔
4697
        }
4✔
4698

4699
        SECTION("Wait to bootstrap all pending subscriptions even when subscription_initializer is not used") {
10✔
4700
            // Client 1
4701
            {
2✔
4702
                auto realm = Realm::get_shared_realm(config);
2✔
4703
                // Create subscription (version = 1) and bootstrap data.
4704
                subscribe_to_all_and_bootstrap(*realm);
2✔
4705
                realm->sync_session()->shutdown_and_wait();
2✔
4706

4707
                // Create a new subscription (version = 2) while the session is closed.
4708
                // The new subscription does not match the object bootstrapped at version 1.
4709
                auto mutable_subscription = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4710
                mutable_subscription.clear();
2✔
4711
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
4712
                auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
4713
                mutable_subscription.insert_or_assign(
2✔
4714
                    Query(table).not_equal(queryable_int_field, foo_obj_queryable_int));
2✔
4715
                mutable_subscription.commit();
2✔
4716

4717
                realm->close();
2✔
4718
            }
2✔
4719

4720
            REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
2!
4721

4722
            // Client 2 uploads data matching Client 1's subscription at version 1
4723
            harness.load_initial_data([&](SharedRealm realm) {
2✔
4724
                CppContext c(realm);
2✔
4725
                Object::create(c, realm, "TopLevel",
2✔
4726
                               std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
4727
                                                {"queryable_str_field", "bar"s},
2✔
4728
                                                {"queryable_int_field", 2 * foo_obj_queryable_int},
2✔
4729
                                                {"non_queryable_field", "some data"s}}));
2✔
4730
            });
2✔
4731

4732
            // Client 1 opens the realm asynchronously and expects the task to complete
4733
            // when the subscription at version 2 finishes bootstrapping.
4734
            auto realm = successfully_async_open_realm(config);
2✔
4735

4736
            // Check subscription at version 2 is marked Complete.
4737
            CHECK(realm->get_latest_subscription_set().version() == 2);
2!
4738
            CHECK(realm->get_active_subscription_set().version() == 2);
2!
4739
        }
2✔
4740
    }
10✔
4741
}
14✔
4742
TEST_CASE("flx sync: Client reset during async open", "[sync][flx][client reset][async open][baas]") {
2✔
4743
    FLXSyncTestHarness harness("flx_bootstrap_reset");
2✔
4744
    auto foo_obj_id = ObjectId::gen();
2✔
4745
    std::atomic<bool> subscription_invoked = false;
2✔
4746
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4747
        CppContext c(realm);
2✔
4748
        Object::create(c, realm, "TopLevel",
2✔
4749
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
4750
                                        {"queryable_str_field", "foo"s},
2✔
4751
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4752
                                        {"non_queryable_field", "created as initial data seed"s}}));
2✔
4753
    });
2✔
4754
    SyncTestFile realm_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4755

4756
    auto subscription_callback = [&](std::shared_ptr<Realm> realm) {
2✔
4757
        REQUIRE(realm);
2!
4758
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
4759
        Query query(table);
2✔
4760
        auto subscription = realm->get_latest_subscription_set();
2✔
4761
        auto mutable_subscription = subscription.make_mutable_copy();
2✔
4762
        mutable_subscription.insert_or_assign(query);
2✔
4763
        subscription_invoked = true;
2✔
4764
        mutable_subscription.commit();
2✔
4765
    };
2✔
4766

4767
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
4768
    realm_config.sync_config->subscription_initializer = subscription_callback;
2✔
4769

4770
    bool client_reset_triggered = false;
2✔
4771
    realm_config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_sess,
2✔
4772
                                                              const SyncClientHookData& event_data) {
78✔
4773
        auto sess = weak_sess.lock();
78✔
4774
        if (!sess) {
78✔
4775
            return SyncClientHookAction::NoAction;
×
4776
        }
×
4777
        if (sess->path() != realm_config.path) {
78✔
4778
            return SyncClientHookAction::NoAction;
28✔
4779
        }
28✔
4780

4781
        if (event_data.event != SyncClientHookEvent::DownloadMessageReceived) {
50✔
4782
            return SyncClientHookAction::NoAction;
44✔
4783
        }
44✔
4784

4785
        if (client_reset_triggered) {
6✔
4786
            return SyncClientHookAction::NoAction;
4✔
4787
        }
4✔
4788

4789
        client_reset_triggered = true;
2✔
4790
        reset_utils::trigger_client_reset(harness.session().app_session(), *sess);
2✔
4791
        return SyncClientHookAction::SuspendWithRetryableError;
2✔
4792
    };
6✔
4793

4794
    auto before_callback_called = util::make_promise_future<void>();
2✔
4795
    realm_config.sync_config->notify_before_client_reset = [&](std::shared_ptr<Realm> realm) {
2✔
4796
        CHECK(realm->schema_version() == 0);
2!
4797
        before_callback_called.promise.emplace_value();
2✔
4798
    };
2✔
4799

4800
    auto after_callback_called = util::make_promise_future<void>();
2✔
4801
    realm_config.sync_config->notify_after_client_reset = [&](std::shared_ptr<Realm> realm, ThreadSafeReference,
2✔
4802
                                                              bool) {
2✔
4803
        CHECK(realm->schema_version() == 0);
2!
4804
        after_callback_called.promise.emplace_value();
2✔
4805
    };
2✔
4806

4807
    auto realm_task = Realm::get_synchronized_realm(realm_config);
2✔
4808
    auto realm_future = realm_task->start();
2✔
4809
    auto realm = Realm::get_shared_realm(std::move(realm_future).get(), util::Scheduler::make_dummy());
2✔
4810
    before_callback_called.future.get();
2✔
4811
    after_callback_called.future.get();
2✔
4812
    REQUIRE(subscription_invoked.load());
2!
4813
}
2✔
4814

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

4819
    std::vector<ObjectId> obj_ids_at_end = fill_large_array_schema(harness);
2✔
4820
    SyncTestFile interrupted_realm_config(harness.app()->current_user(), harness.schema(),
2✔
4821
                                          SyncConfig::FLXSyncEnabled{});
2✔
4822

4823
    {
2✔
4824
        auto pf = util::make_promise_future<void>();
2✔
4825
        Realm::Config config = interrupted_realm_config;
2✔
4826
        config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
4827
        config.sync_config->on_sync_client_event_hook =
2✔
4828
            [promise = util::CopyablePromiseHolder(std::move(pf.promise))](std::weak_ptr<SyncSession> weak_session,
2✔
4829
                                                                           const SyncClientHookData& data) mutable {
78✔
4830
                if (data.event != SyncClientHookEvent::BootstrapMessageProcessed &&
78✔
4831
                    data.event != SyncClientHookEvent::BootstrapProcessed) {
78✔
4832
                    return SyncClientHookAction::NoAction;
60✔
4833
                }
60✔
4834
                auto session = weak_session.lock();
18✔
4835
                if (!session) {
18✔
4836
                    return SyncClientHookAction::NoAction;
×
4837
                }
×
4838
                if (data.query_version != 1) {
18✔
4839
                    return SyncClientHookAction::NoAction;
4✔
4840
                }
4✔
4841

4842
                // Commit a subscriptions set whenever a bootstrap message is received for query version 1.
4843
                if (data.event == SyncClientHookEvent::BootstrapMessageProcessed) {
14✔
4844
                    auto latest_subs = session->get_flx_subscription_store()->get_latest().make_mutable_copy();
12✔
4845
                    latest_subs.commit();
12✔
4846
                    return SyncClientHookAction::NoAction;
12✔
4847
                }
12✔
4848
                // At least one subscription set was created.
4849
                CHECK(session->get_flx_subscription_store()->get_latest().version() > 1);
2!
4850
                promise.get_promise().emplace_value();
2✔
4851
                // Reconnect once query version 1 is bootstrapped.
4852
                return SyncClientHookAction::TriggerReconnect;
2✔
4853
            };
2✔
4854

4855
        auto realm = Realm::get_shared_realm(config);
2✔
4856
        {
2✔
4857
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4858
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
4859
            mut_subs.insert_or_assign(Query(table));
2✔
4860
            mut_subs.commit();
2✔
4861
        }
2✔
4862
        pf.future.get();
2✔
4863
        realm->sync_session()->shutdown_and_wait();
2✔
4864
        realm->close();
2✔
4865
    }
2✔
4866

4867
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
4868

4869
    // Check at least one subscription set needs to be resent.
4870
    {
2✔
4871
        DBOptions options;
2✔
4872
        options.encryption_key = test_util::crypt_key();
2✔
4873
        auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
4874
        auto sub_store = sync::SubscriptionStore::create(realm);
2✔
4875
        auto version_info = sub_store->get_version_info();
2✔
4876
        REQUIRE(version_info.latest > version_info.active);
2!
4877
    }
2✔
4878

4879
    // Resend the pending subscriptions.
4880
    auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
4881
    wait_for_upload(*realm);
2✔
4882
    wait_for_download(*realm);
2✔
4883
}
2✔
4884

4885
TEST_CASE("flx: fatal errors and session becoming inactive cancel pending waits", "[sync][flx][baas]") {
2✔
4886
    std::vector<ObjectSchema> schema{
2✔
4887
        {"TopLevel",
2✔
4888
         {
2✔
4889
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
4890
             {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
2✔
4891
         }},
2✔
4892
    };
2✔
4893

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

4897
    auto check_status = [](auto status) {
4✔
4898
        CHECK(!status.is_ok());
4!
4899
        std::string reason = status.get_status().reason();
4✔
4900
        // Subscription notification is cancelled either because the sync session is inactive, or because a fatal
4901
        // error is received from the server.
4902
        if (reason.find("Sync session became inactive") == std::string::npos &&
4✔
4903
            reason.find("Invalid schema change (UPLOAD): non-breaking schema change: adding \"Int\" column at field "
4✔
4904
                        "\"other_col\" in schema \"TopLevel\", schema changes from clients are restricted when "
2✔
4905
                        "developer mode is disabled") == std::string::npos) {
2✔
4906
            FAIL(reason);
×
4907
        }
×
4908
    };
4✔
4909

4910
    auto create_subscription = [](auto realm) -> realm::sync::SubscriptionSet {
4✔
4911
        auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
4✔
4912
        auto table = realm->read_group().get_table("class_TopLevel");
4✔
4913
        mut_subs.insert_or_assign(Query(table));
4✔
4914
        return mut_subs.commit();
4✔
4915
    };
4✔
4916

4917
    auto [error_occured_promise, error_occurred] = util::make_promise_future<void>();
2✔
4918
    config.sync_config->error_handler = [promise = util::CopyablePromiseHolder(std::move(error_occured_promise))](
2✔
4919
                                            std::shared_ptr<SyncSession>, SyncError) mutable {
2✔
4920
        promise.get_promise().emplace_value();
2✔
4921
    };
2✔
4922

4923
    auto realm = Realm::get_shared_realm(config);
2✔
4924
    wait_for_download(*realm);
2✔
4925

4926
    auto subs = create_subscription(realm);
2✔
4927
    auto subs_future = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
4928

4929
    realm->sync_session()->pause();
2✔
4930
    auto state = subs_future.get_no_throw();
2✔
4931
    check_status(state);
2✔
4932

4933
    auto [download_complete_promise, download_complete] = util::make_promise_future<void>();
2✔
4934
    realm->sync_session()->wait_for_upload_completion([promise = std::move(download_complete_promise)](auto) mutable {
2✔
4935
        promise.emplace_value();
2✔
4936
    });
2✔
4937
    schema[0].persisted_properties.push_back({"other_col", PropertyType::Int | PropertyType::Nullable});
2✔
4938
    realm->update_schema(schema);
2✔
4939

4940
    subs = create_subscription(realm);
2✔
4941
    subs_future = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
4942

4943
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4944
        CppContext c(realm);
2✔
4945
        Object::create(c, realm, "TopLevel",
2✔
4946
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
4947
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4948
                                        {"other_col", static_cast<int64_t>(42)}}));
2✔
4949
    });
2✔
4950

4951
    realm->sync_session()->resume();
2✔
4952
    download_complete.get();
2✔
4953
    error_occurred.get();
2✔
4954
    state = subs_future.get_no_throw();
2✔
4955
    check_status(state);
2✔
4956
}
2✔
4957

4958
TEST_CASE("flx: pause and resume bootstrapping at query version 0", "[sync][flx][baas]") {
2✔
4959
    FLXSyncTestHarness harness("flx_pause_resume_bootstrap");
2✔
4960
    SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4961
    auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
4962
    std::mutex download_message_mutex;
2✔
4963
    int download_message_integrated_count = 0;
2✔
4964
    triggered_config.sync_config->on_sync_client_event_hook =
2✔
4965
        [promise = util::CopyablePromiseHolder(std::move(interrupted_promise)), &download_message_integrated_count,
2✔
4966
         &download_message_mutex](std::weak_ptr<SyncSession> weak_sess, const SyncClientHookData& data) mutable {
34✔
4967
            auto sess = weak_sess.lock();
34✔
4968
            if (!sess || data.event != SyncClientHookEvent::DownloadMessageIntegrated) {
34✔
4969
                return SyncClientHookAction::NoAction;
30✔
4970
            }
30✔
4971

4972
            std::lock_guard<std::mutex> lk(download_message_mutex);
4✔
4973
            // Pause and resume the first session after the bootstrap message is integrated.
4974
            if (download_message_integrated_count == 0) {
4✔
4975
                sess->pause();
2✔
4976
                sess->resume();
2✔
4977
            }
2✔
4978
            // Complete the test when the second session integrates the empty download
4979
            // message it receives.
4980
            else {
2✔
4981
                promise.get_promise().emplace_value();
2✔
4982
            }
2✔
4983
            ++download_message_integrated_count;
4✔
4984
            return SyncClientHookAction::NoAction;
4✔
4985
        };
34✔
4986
    auto realm = Realm::get_shared_realm(triggered_config);
2✔
4987
    interrupted.get();
2✔
4988
    std::lock_guard<std::mutex> lk(download_message_mutex);
2✔
4989
    CHECK(download_message_integrated_count == 2);
2!
4990
    auto active_sub_set = realm->sync_session()->get_flx_subscription_store()->get_active();
2✔
4991
    REQUIRE(active_sub_set.version() == 0);
2!
4992
    REQUIRE(active_sub_set.state() == sync::SubscriptionSet::State::Complete);
2!
4993
}
2✔
4994

4995
TEST_CASE("flx: collections in mixed - merge lists", "[sync][flx][baas]") {
2✔
4996
    Schema schema{{"TopLevel",
2✔
4997
                   {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
4998
                    {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
4999
                    {"any", PropertyType::Mixed | PropertyType::Nullable}}}};
2✔
5000

5001
    FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}};
2✔
5002
    FLXSyncTestHarness harness("flx_collections_in_mixed", server_schema);
2✔
5003
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
5004

5005
    auto set_list_and_insert_element = [](Obj& obj, ColKey col_any, Mixed value) {
8✔
5006
        obj.set_collection(col_any, CollectionType::List);
8✔
5007
        auto list = obj.get_list_ptr<Mixed>(col_any);
8✔
5008
        list->add(value);
8✔
5009
    };
8✔
5010

5011
    // Client 1 creates an object and sets property 'any' to an integer value.
5012
    auto foo_obj_id = ObjectId::gen();
2✔
5013
    harness.load_initial_data([&](SharedRealm realm) {
2✔
5014
        CppContext c(realm);
2✔
5015
        Object::create(c, realm, "TopLevel",
2✔
5016
                       std::any(AnyDict{{"_id", foo_obj_id}, {"queryable_str_field", "foo"s}, {"any", 42}}));
2✔
5017
    });
2✔
5018

5019
    // Client 2 opens the realm and downloads schema and object created by Client 1.
5020
    auto realm = Realm::get_shared_realm(config);
2✔
5021
    subscribe_to_all_and_bootstrap(*realm);
2✔
5022
    realm->sync_session()->pause();
2✔
5023

5024
    // Client 3 sets property 'any' to List and inserts two integers in the list.
5025
    harness.load_initial_data([&](SharedRealm realm) {
2✔
5026
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
5027
        auto obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
5028
        auto col_any = table->get_column_key("any");
2✔
5029
        set_list_and_insert_element(obj, col_any, 1);
2✔
5030
        set_list_and_insert_element(obj, col_any, 2);
2✔
5031
    });
2✔
5032

5033
    // While its session is paused, Client 2 sets property 'any' to List and inserts two integers in the list.
5034
    CppContext c(realm);
2✔
5035
    realm->begin_transaction();
2✔
5036
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
5037
    auto obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
5038
    auto col_any = table->get_column_key("any");
2✔
5039
    set_list_and_insert_element(obj, col_any, 3);
2✔
5040
    set_list_and_insert_element(obj, col_any, 4);
2✔
5041
    realm->commit_transaction();
2✔
5042

5043
    realm->sync_session()->resume();
2✔
5044
    wait_for_upload(*realm);
2✔
5045
    wait_for_download(*realm);
2✔
5046
    wait_for_advance(*realm);
2✔
5047

5048
    // Client 2 ends up with four integers in the list (in the correct order).
5049
    auto list = obj.get_list_ptr<Mixed>(col_any);
2✔
5050
    CHECK(list->size() == 4);
2!
5051
    CHECK(list->get(0) == 1);
2!
5052
    CHECK(list->get(1) == 2);
2!
5053
    CHECK(list->get(2) == 3);
2!
5054
    CHECK(list->get(3) == 4);
2!
5055
}
2✔
5056

5057
TEST_CASE("flx: nested collections in mixed", "[sync][flx][baas]") {
2✔
5058
    Schema schema{{"TopLevel",
2✔
5059
                   {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
5060
                    {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
5061
                    {"any", PropertyType::Mixed | PropertyType::Nullable}}}};
2✔
5062

5063
    FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}};
2✔
5064
    FLXSyncTestHarness harness("flx_collections_in_mixed", server_schema);
2✔
5065
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
5066

5067
    // Client 1: {_id: 1, any: ["abc", [42]]}
5068
    harness.load_initial_data([&](SharedRealm realm) {
2✔
5069
        CppContext c(realm);
2✔
5070
        auto obj = Object::create(c, realm, "TopLevel",
2✔
5071
                                  std::any(AnyDict{{"_id", INT64_C(1)}, {"queryable_str_field", "foo"s}}));
2✔
5072
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
5073
        auto col_any = table->get_column_key("any");
2✔
5074
        obj.get_obj().set_collection(col_any, CollectionType::List);
2✔
5075
        List list(obj, obj.get_object_schema().property_for_name("any"));
2✔
5076
        list.insert_any(0, "abc");
2✔
5077
        list.insert_collection(1, CollectionType::List);
2✔
5078
        auto list2 = list.get_list(1);
2✔
5079
        list2.insert_any(0, 42);
2✔
5080
    });
2✔
5081

5082
    // Client 2 opens the realm and downloads schema and object created by Client 1.
5083
    // {_id: 1, any: ["abc", [42]]}
5084
    auto realm = Realm::get_shared_realm(config);
2✔
5085
    subscribe_to_all_and_bootstrap(*realm);
2✔
5086
    realm->sync_session()->pause();
2✔
5087

5088
    // Client 3 adds a dictionary with an element to list 'any'
5089
    // {_id: 1, any: [{{"key": 6}}, "abc", [42]]}
5090
    harness.load_initial_data([&](SharedRealm realm) {
2✔
5091
        CppContext c(realm);
2✔
5092
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(INT64_C(1)));
2✔
5093
        List list(obj, obj.get_object_schema().property_for_name("any"));
2✔
5094
        list.insert_collection(PathElement(0), CollectionType::Dictionary);
2✔
5095
        auto dict = list.get_dictionary(PathElement(0));
2✔
5096
        dict.insert_any("key", INT64_C(6));
2✔
5097
    });
2✔
5098

5099
    // While its session is paused, Client 2 makes a change to a nested list
5100
    // {_id: 1, any: ["abc", [42, "foo"]]}
5101
    CppContext c(realm);
2✔
5102
    realm->begin_transaction();
2✔
5103
    auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(INT64_C(1)));
2✔
5104
    List list(obj, obj.get_object_schema().property_for_name("any"));
2✔
5105
    List list2 = list.get_list(PathElement(1));
2✔
5106
    list2.insert_any(list2.size(), "foo");
2✔
5107
    realm->commit_transaction();
2✔
5108

5109
    realm->sync_session()->resume();
2✔
5110
    wait_for_upload(*realm);
2✔
5111
    wait_for_download(*realm);
2✔
5112
    wait_for_advance(*realm);
2✔
5113

5114
    // Client 2 after the session is resumed
5115
    // {_id: 1, any: [{{"key": 6}}, "abc", [42, "foo"]]}
5116
    CHECK(list.size() == 3);
2!
5117
    auto nested_dict = list.get_dictionary(0);
2✔
5118
    CHECK(nested_dict.size() == 1);
2!
5119
    CHECK(nested_dict.get<Int>("key") == 6);
2!
5120

5121
    CHECK(list.get_any(1) == "abc");
2!
5122

5123
    auto nested_list = list.get_list(2);
2✔
5124
    CHECK(nested_list.size() == 2);
2!
5125
    CHECK(nested_list.get_any(0) == 42);
2!
5126
    CHECK(nested_list.get_any(1) == "foo");
2!
5127
}
2✔
5128

5129
TEST_CASE("flx: no upload during bootstraps", "[sync][flx][bootstrap][baas]") {
2✔
5130
    FLXSyncTestHarness harness("flx_bootstrap_no_upload", {g_large_array_schema, {"queryable_int_field"}});
2✔
5131
    fill_large_array_schema(harness);
2✔
5132
    auto config = harness.make_test_file();
2✔
5133
    enum class TestState { Start, BootstrapInProgress, BootstrapProcessed, BootstrapAck };
2✔
5134
    TestingStateMachine<TestState> state(TestState::Start);
2✔
5135
    config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>, const SyncClientHookData& data) {
80✔
5136
        if (data.query_version == 0) {
80✔
5137
            return SyncClientHookAction::NoAction;
24✔
5138
        }
24✔
5139
        state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
56✔
5140
            // Check no upload messages are sent during bootstrap.
5141
            if (data.event == SyncClientHookEvent::BootstrapMessageProcessed) {
56✔
5142
                CHECK((cur_state == TestState::Start || cur_state == TestState::BootstrapInProgress));
12!
5143
                return TestState::BootstrapInProgress;
12✔
5144
            }
12✔
5145
            else if (data.event == SyncClientHookEvent::DownloadMessageIntegrated &&
44✔
5146
                     data.batch_state == sync::DownloadBatchState::LastInBatch) {
44✔
5147
                CHECK(cur_state == TestState::BootstrapInProgress);
2!
5148
                return TestState::BootstrapProcessed;
2✔
5149
            }
2✔
5150
            else if (data.event == SyncClientHookEvent::UploadMessageSent) {
42✔
5151
                // Uploads are allowed before a bootstrap starts.
5152
                if (cur_state == TestState::Start) {
6✔
5153
                    return std::nullopt; // Don't transition
4✔
5154
                }
4✔
5155
                CHECK(cur_state == TestState::BootstrapProcessed);
2!
5156
                return TestState::BootstrapAck;
2✔
5157
            }
2✔
5158
            return std::nullopt;
36✔
5159
        });
56✔
5160
        return SyncClientHookAction::NoAction;
56✔
5161
    };
80✔
5162

5163
    auto realm = Realm::get_shared_realm(config);
2✔
5164
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
5165
    auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
5166
    new_subs.insert_or_assign(Query(table));
2✔
5167
    new_subs.commit();
2✔
5168
    state.wait_for(TestState::BootstrapAck);
2✔
5169

5170
    // Commiting an empty changeset does not upload a message.
5171
    realm->begin_transaction();
2✔
5172
    realm->commit_transaction();
2✔
5173
    // Give the sync client the chance to send an upload after mark.
5174
    wait_for_download(*realm);
2✔
5175
}
2✔
5176

5177
} // namespace realm::app
5178

5179
#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