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

realm / realm-core / jorgen.edelbo_402

21 Aug 2024 11:10AM UTC coverage: 91.054% (-0.03%) from 91.085%
jorgen.edelbo_402

Pull #7803

Evergreen

jedelbo
Small fix to Table::typed_write

When writing the realm to a new file from a write transaction,
the Table may be COW so that the top ref is changed. So don't
use the ref that is present in the group when the operation starts.
Pull Request #7803: Feature/string compression

103494 of 181580 branches covered (57.0%)

1929 of 1999 new or added lines in 46 files covered. (96.5%)

695 existing lines in 51 files now uncovered.

220142 of 241772 relevant lines covered (91.05%)

7344461.76 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,833✔
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,833✔
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()) {
417✔
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())) {
417✔
596
                    FAIL("UPLOAD messages are not allowed during client reset fresh realm download");
×
597
                }
×
598
            }
417✔
599
        }
417✔
600
        return SyncClientHookAction::NoAction;
2,833✔
601
    };
2,833✔
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,889✔
2684
        std::lock_guard<std::mutex> lk(sync_error_mutex);
2,889✔
2685
        return static_cast<bool>(sync_error);
2,889✔
2686
    });
2,889✔
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([&] {
20,440✔
2717
        std::lock_guard lk(sync_error_mutex);
20,440✔
2718
        return static_cast<bool>(sync_error);
20,440✔
2719
    });
20,440✔
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 {
24✔
4133
        auto session = weak_session.lock();
24✔
4134
        if (!session) {
24✔
4135
            return SyncClientHookAction::NoAction;
×
4136
        }
×
4137

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

4143
            return SyncClientHookAction::NoAction;
20✔
4144
        }
20✔
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) {
81✔
4217
        if (auto session = weak_session.lock(); !session) {
81✔
UNCOV
4218
            return SyncClientHookAction::NoAction;
×
UNCOV
4219
        }
×
4220
        SyncClientHookAction action = SyncClientHookAction::NoAction;
81✔
4221
        state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
81✔
4222
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
81✔
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 &&
71✔
4226
                    (cur_state == TestState::FirstError || cur_state == TestState::SecondError)) {
71✔
4227
                    action = SyncClientHookAction::EarlyReturn;
2✔
4228
                }
2✔
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;
69✔
4237
            }
71✔
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;
81✔
4268
    };
81✔
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