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

realm / realm-core / 2517

24 Jul 2024 01:12PM UTC coverage: 91.077% (+0.007%) from 91.07%
2517

push

Evergreen

web-flow
Fix intermittent failure of the role change client reset tests (#7919)

102674 of 181450 branches covered (56.59%)

1 of 3 new or added lines in 1 file covered. (33.33%)

45 existing lines in 16 files now uncovered.

216300 of 237491 relevant lines covered (91.08%)

5560480.31 hits per line

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

95.55
/test/object-store/sync/flx_role_change.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2024 MongoDB 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
#ifdef REALM_ENABLE_AUTH_TESTS
20

21
#include <catch2/catch_all.hpp>
22

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

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

30
#include <realm/object-store/impl/realm_coordinator.hpp>
31
#include <realm/object-store/object.hpp>
32
#include <realm/object-store/schema.hpp>
33
#include <realm/object-store/impl/object_accessor_impl.hpp>
34
#include <realm/object-store/sync/async_open_task.hpp>
35
#include <realm/object-store/sync/sync_session.hpp>
36

37
#include <realm/sync/client_base.hpp>
38
#include <realm/sync/config.hpp>
39
#include <realm/sync/protocol.hpp>
40
#include <realm/sync/subscriptions.hpp>
41
#include <realm/sync/noinst/client_reset_operation.hpp>
42

43
#include <realm/util/future.hpp>
44
#include <realm/util/logger.hpp>
45

46
#include <filesystem>
47
#include <iostream>
48
#include <stdexcept>
49

50
using namespace realm;
51
using namespace realm::app;
52

53
namespace {
54

55
const Schema g_person_schema{{"Person",
56
                              {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
57
                               {"role", PropertyType::String},
58
                               {"name", PropertyType::String},
59
                               {"emp_id", PropertyType::Int}}}};
60

61
auto fill_person_schema = [](SharedRealm realm, std::string role, size_t count) {
18✔
62
    CppContext c(realm);
18✔
63
    for (size_t i = 0; i < count; ++i) {
1,158✔
64
        auto obj = Object::create(c, realm, "Person",
1,140✔
65
                                  std::any(AnyDict{
1,140✔
66
                                      {"_id", ObjectId::gen()},
1,140✔
67
                                      {"role", role},
1,140✔
68
                                      {"name", util::format("%1-%2", role, i)},
1,140✔
69
                                      {"emp_id", static_cast<int64_t>(i)},
1,140✔
70
                                  }));
1,140✔
71
    }
1,140✔
72
};
18✔
73

74
struct HarnessParams {
75
    size_t num_emps = 150;
76
    size_t num_mgrs = 25;
77
    size_t num_dirs = 10;
78
    std::optional<size_t> num_objects = 10;
79
    std::optional<size_t> max_download_bytes = 4096;
80
    std::optional<size_t> sleep_millis;
81
};
82

83
std::unique_ptr<FLXSyncTestHarness> setup_harness(std::string app_name, HarnessParams params)
84
{
6✔
85
    auto harness = std::make_unique<FLXSyncTestHarness>(
6✔
86
        app_name, FLXSyncTestHarness::ServerSchema{g_person_schema, {"role", "name"}});
6✔
87

88
    auto& app_session = harness->session().app_session();
6✔
89

90
    if (params.num_objects) {
6✔
91
        REQUIRE(app_session.admin_api.patch_app_settings(
6!
92
            app_session.server_app_id, {{"sync", {{"num_objects_before_bootstrap_flush", *params.num_objects}}}}));
6✔
93
    }
6✔
94

95
    if (params.max_download_bytes) {
6✔
96
        REQUIRE(app_session.admin_api.patch_app_settings(
6!
97
            app_session.server_app_id,
6✔
98
            {{"sync", {{"qbs_download_changeset_soft_max_byte_size", *params.max_download_bytes}}}}));
6✔
99
    }
6✔
100

101
    if (params.sleep_millis) {
6✔
102
        REQUIRE(app_session.admin_api.patch_app_settings(
×
103
            app_session.server_app_id, {{"sync", {{"download_loop_sleep_millis", *params.sleep_millis}}}}));
×
104
    }
×
105

106
    // Initialize the realm with some data
107
    harness->load_initial_data([&](SharedRealm realm) {
6✔
108
        fill_person_schema(realm, "employee", params.num_emps);
6✔
109
        fill_person_schema(realm, "manager", params.num_mgrs);
6✔
110
        fill_person_schema(realm, "director", params.num_dirs);
6✔
111
    });
6✔
112
    // Return the unique_ptr for the newly created harness
113
    return harness;
6✔
114
}
6✔
115

116
void update_role(nlohmann::json& rule, nlohmann::json doc_filter)
117
{
82✔
118
    rule["roles"][0]["document_filters"]["read"] = doc_filter;
82✔
119
    rule["roles"][0]["document_filters"]["write"] = doc_filter;
82✔
120
}
82✔
121

122
void set_up_realm(SharedRealm& setup_realm, size_t expected_cnt)
123
{
12✔
124
    // Set up the initial subscription
125
    auto table = setup_realm->read_group().get_table("class_Person");
12✔
126
    auto new_subs = setup_realm->get_latest_subscription_set().make_mutable_copy();
12✔
127
    new_subs.insert_or_assign(Query(table));
12✔
128
    auto subs = new_subs.commit();
12✔
129

130
    // Wait for subscription update and sync to complete
131
    subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
12✔
132
    REQUIRE(!wait_for_download(*setup_realm));
12!
133
    REQUIRE(!wait_for_upload(*setup_realm));
12!
134
    wait_for_advance(*setup_realm);
12✔
135

136
    // Verify the data was downloaded
137
    table = setup_realm->read_group().get_table("class_Person");
12✔
138
    Results results(setup_realm, Query(table));
12✔
139
    REQUIRE(results.size() == expected_cnt);
12!
140
}
12✔
141

142
void verify_records(SharedRealm& check_realm, size_t emps, size_t mgrs, size_t dirs)
143
{
80✔
144
    // Validate the expected number of entries for each role type after the role change
145
    auto table = check_realm->read_group().get_table("class_Person");
80✔
146
    REQUIRE(table->size() == (emps + mgrs + dirs));
80!
147
    auto role_col = table->get_column_key("role");
80✔
148
    auto table_query = Query(table).equal(role_col, "employee");
80✔
149
    auto results = Results(check_realm, table_query);
80✔
150
    REQUIRE(results.size() == emps);
80!
151
    table_query = Query(table).equal(role_col, "manager");
80✔
152
    results = Results(check_realm, table_query);
80✔
153
    REQUIRE(results.size() == mgrs);
80!
154
    table_query = Query(table).equal(role_col, "director");
80✔
155
    results = Results(check_realm, table_query);
80✔
156
    REQUIRE(results.size() == dirs);
80!
157
}
80✔
158

159
// Wait for realm download/upload/advance and then validate the record counts in the
160
// local realm.
161
bool wait_and_verify(SharedRealm realm, size_t emps, size_t mgrs, size_t dirs)
162
{
76✔
163
    // Using a bool to return the wait results, since using REQUIRE around
164
    // wait_for_download() and wait_for_upload() was causing TSAN errors
165
    // with the REQUIRE calls in the event hook
166
    if (wait_for_download(*realm) || wait_for_upload(*realm)) // these return true on failure
76✔
167
        return false;
×
168
    wait_for_advance(*realm);
76✔
169
    verify_records(realm, emps, mgrs, dirs);
76✔
170
    return true;
76✔
171
}
76✔
172

173
} // namespace
174

175
TEST_CASE("flx: role change bootstraps", "[sync][flx][baas][role change][bootstrap]") {
6✔
176
    auto logger = util::Logger::get_default_logger();
6✔
177

178
    // Pausing the download builder will ensure a single download message (for the bootstrap
179
    // in this test), will contain all the changesets for the bootstrap.
180
    auto pause_download_builder = [](std::weak_ptr<SyncSession> weak_session, bool pause) {
6✔
181
        if (auto session = weak_session.lock()) {
4✔
182
            nlohmann::json test_command = {{"command", pause ? "PAUSE_DOWNLOAD_BUILDER" : "RESUME_DOWNLOAD_BUILDER"}};
4✔
183
            SyncSession::OnlyForTesting::send_test_command(*session, test_command.dump())
4✔
184
                .get_async([](StatusWith<std::string> result) {
4✔
185
                    REQUIRE(result.is_ok());             // Future completed successfully
4!
186
                    REQUIRE(result.get_value() == "{}"); // Command completed successfully
4!
187
                });
4✔
188
        }
4✔
189
    };
4✔
190

191
    enum BootstrapMode {
6✔
192
        NoErrorNoBootstrap,
6✔
193
        GotErrorNoBootstrap,
6✔
194
        SingleMessage,
6✔
195
        SingleMessageMulti,
6✔
196
        MultiMessage,
6✔
197
        AnyBootstrap
6✔
198
    };
6✔
199
    struct ExpectedResults {
6✔
200
        BootstrapMode bootstrap;
6✔
201
        size_t emps;
6✔
202
        size_t mgrs;
6✔
203
        size_t dirs;
6✔
204
    };
6✔
205

206
    enum TestState {
6✔
207
        not_ready,
6✔
208
        start,
6✔
209
        reconnect_received,
6✔
210
        session_resumed,
6✔
211
        ident_message,
6✔
212
        downloading,
6✔
213
        downloaded,
6✔
214
        complete
6✔
215
    };
6✔
216

217
    TestingStateMachine<TestState> state_machina(TestState::not_ready);
6✔
218
    int64_t query_version = 0;
6✔
219
    BootstrapMode bootstrap_mode = BootstrapMode::GotErrorNoBootstrap;
6✔
220
    size_t download_msg_count = 0;
6✔
221
    size_t bootstrap_msg_count = 0;
6✔
222
    bool role_change_bootstrap = false;
6✔
223
    bool send_test_command = false;
6✔
224

225
    auto setup_config_callbacks = [&](SyncTestFile& config) {
6✔
226
        // Use the sync client event hook to check for the error received and for tracking
227
        // download messages and bootstraps
228
        config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
6✔
229
                                                            const SyncClientHookData& data) {
362✔
230
            state_machina.transition_with([&](TestState cur_state) -> std::optional<TestState> {
362✔
231
                if (cur_state == TestState::not_ready || cur_state == TestState::complete)
362✔
232
                    return std::nullopt;
194✔
233

234
                using BatchState = sync::DownloadBatchState;
168✔
235
                using Event = SyncClientHookEvent;
168✔
236
                switch (data.event) {
168✔
237
                    case Event::ErrorMessageReceived:
12✔
238
                        REQUIRE(cur_state == TestState::start);
12!
239
                        REQUIRE(data.error_info->raw_error_code ==
12!
240
                                static_cast<int>(sync::ProtocolError::session_closed));
12✔
241
                        REQUIRE(data.error_info->server_requests_action ==
12!
242
                                sync::ProtocolErrorInfo::Action::Transient);
12✔
243
                        REQUIRE_FALSE(data.error_info->is_fatal);
12!
244
                        return TestState::reconnect_received;
12✔
245

246
                    case Event::SessionConnected:
✔
247
                        // Handle the reconnect if session multiplexing is disabled
248
                        [[fallthrough]];
×
249
                    case Event::SessionResumed:
12✔
250
                        if (send_test_command) {
12✔
251
                            REQUIRE(cur_state == TestState::reconnect_received);
2!
252
                            logger->trace("ROLE CHANGE: sending PAUSE test command after resumed");
2✔
253
                            pause_download_builder(weak_session, true);
2✔
254
                        }
2✔
255
                        return TestState::session_resumed;
12✔
256

257
                    case Event::IdentMessageSent:
12✔
258
                        if (send_test_command) {
12✔
259
                            REQUIRE(cur_state == TestState::session_resumed);
2!
260
                            logger->trace("ROLE CHANGE: sending RESUME test command after ident message sent");
2✔
261
                            pause_download_builder(weak_session, false);
2✔
262
                        }
2✔
263
                        return TestState::ident_message;
12✔
264

265
                    case Event::DownloadMessageReceived: {
24✔
266
                        // Skip unexpected download messages
267
                        if (cur_state != TestState::ident_message && cur_state != TestState::downloading) {
24✔
268
                            return std::nullopt;
×
269
                        }
×
270
                        ++download_msg_count;
24✔
271
                        // A multi-message bootstrap is in progress..
272
                        if (data.batch_state == BatchState::MoreToCome) {
24✔
273
                            // More than 1 bootstrap message, always a multi-message
274
                            bootstrap_mode = BootstrapMode::MultiMessage;
12✔
275
                            logger->trace("ROLE CHANGE: detected multi-message bootstrap");
12✔
276
                            return TestState::downloading;
12✔
277
                        }
12✔
278
                        // single bootstrap message or last message in the multi-message bootstrap
279
                        else if (data.batch_state == BatchState::LastInBatch) {
12✔
280
                            if (download_msg_count == 1) {
12✔
281
                                if (data.num_changesets == 1) {
8✔
282
                                    logger->trace("ROLE CHANGE: detected single-message/single-changeset bootstrap");
6✔
283
                                    bootstrap_mode = BootstrapMode::SingleMessage;
6✔
284
                                }
6✔
285
                                else {
2✔
286
                                    logger->trace("ROLE CHANGE: detected single-message/multi-changeset bootstrap");
2✔
287
                                    bootstrap_mode = BootstrapMode::SingleMessageMulti;
2✔
288
                                }
2✔
289
                            }
8✔
290
                            return TestState::downloaded;
12✔
291
                        }
12✔
292
                        return std::nullopt;
×
293
                    }
24✔
294

295
                    // A bootstrap message was processed
296
                    case Event::BootstrapMessageProcessed: {
24✔
297
                        REQUIRE(data.batch_state != BatchState::SteadyState);
24!
298
                        REQUIRE((cur_state == TestState::downloading || cur_state == TestState::downloaded));
24!
299
                        ++bootstrap_msg_count;
24✔
300
                        if (data.query_version == query_version) {
24✔
301
                            role_change_bootstrap = true;
24✔
302
                        }
24✔
303
                        return std::nullopt;
24✔
304
                    }
24✔
305
                    // The bootstrap has been received and processed
306
                    case Event::BootstrapProcessed:
12✔
307
                        REQUIRE(cur_state == TestState::downloaded);
12!
308
                        return TestState::complete;
12✔
309

310
                    default:
72✔
311
                        return std::nullopt;
72✔
312
                }
168✔
313
            });
168✔
314
            return SyncClientHookAction::NoAction;
362✔
315
        };
362✔
316

317
        // Add client reset callback to verify a client reset doesn't happen
318
        config.sync_config->notify_before_client_reset = [&](std::shared_ptr<Realm>) {
6✔
319
            // Make sure a client reset did not occur while waiting for the role change to
320
            // be applied
321
            FAIL("Client reset is not expected when the role/rules/permissions are changed");
×
322
        };
×
323
    };
6✔
324

325
    auto update_perms_and_verify = [&](FLXSyncTestHarness& harness, SharedRealm check_realm, nlohmann::json new_rules,
6✔
326
                                       ExpectedResults expected) {
14✔
327
        // Reset the state machine
328
        state_machina.transition_with([&](TestState cur_state) {
14✔
329
            REQUIRE(cur_state == TestState::not_ready);
14!
330
            bootstrap_msg_count = 0;
14✔
331
            download_msg_count = 0;
14✔
332
            role_change_bootstrap = false;
14✔
333
            query_version = check_realm->get_active_subscription_set().version();
14✔
334
            if (expected.bootstrap == BootstrapMode::SingleMessageMulti) {
14✔
335
                send_test_command = true;
2✔
336
            }
2✔
337
            return TestState::start;
14✔
338
        });
14✔
339

340
        // Update the permissions on the server - should send an error to the client to force
341
        // it to reconnect
342
        auto& app_session = harness.session().app_session();
14✔
343
        logger->debug("ROLE CHANGE: Updating rule definitions: %1", new_rules);
14✔
344
        app_session.admin_api.update_default_rule(app_session.server_app_id, new_rules);
14✔
345

346
        if (expected.bootstrap != BootstrapMode::NoErrorNoBootstrap) {
14✔
347
            // After updating the permissions (if they are different), the server should send an
348
            // error that will disconnect/reconnect the session - verify the reconnect occurs.
349
            // Make sure at least the reconnect state (or later) has been reached
350
            auto state_reached = state_machina.wait_until([](TestState cur_state) {
12✔
351
                return static_cast<int>(cur_state) >= static_cast<int>(TestState::reconnect_received);
12✔
352
            });
12✔
353
            REQUIRE(state_reached);
12!
354
        }
12✔
355

356
        // Assuming the session disconnects and reconnects, the server initiated role change
357
        // bootstrap download will take place when the session is re-established and will
358
        // complete before the server sends the initial MARK response.
359
        // Validate the expected number of entries for each role type after the role change
360
        bool update_successful = wait_and_verify(check_realm, expected.emps, expected.mgrs, expected.dirs);
14✔
361

362
        // Now that the server initiated bootstrap should be complete, verify the operation
363
        // performed matched what was expected.
364
        state_machina.transition_with([&](TestState cur_state) {
14✔
365
            if (!update_successful) {
14✔
366
                FAIL("Failed to wait for realm update during role change bootstrap");
×
367
            }
×
368

369
            switch (expected.bootstrap) {
14✔
370
                case BootstrapMode::NoErrorNoBootstrap:
2✔
371
                    // Confirm that neither an error nor bootstrap occurred
372
                    REQUIRE(cur_state == TestState::start);
2!
373
                    REQUIRE_FALSE(role_change_bootstrap);
2!
374
                    break;
2✔
375
                case BootstrapMode::GotErrorNoBootstrap:
2✔
376
                    // Confirm that the session restarted, but a bootstrap did not occur
377
                    REQUIRE(cur_state == TestState::reconnect_received);
×
378
                    REQUIRE_FALSE(role_change_bootstrap);
×
379
                    break;
×
380
                case BootstrapMode::AnyBootstrap:
6✔
381
                    // Confirm that a bootstrap occurred, but it doesn't matter which type
382
                    REQUIRE(cur_state == TestState::complete);
6!
383
                    REQUIRE(role_change_bootstrap);
6!
384
                    break;
6✔
385
                default:
6✔
386
                    // By the time the MARK response is received and wait_for_download()
387
                    // returns, the bootstrap should have already been applied.
388
                    REQUIRE(expected.bootstrap == bootstrap_mode);
6!
389
                    REQUIRE(role_change_bootstrap);
6!
390
                    REQUIRE(cur_state == TestState::complete);
6!
391
                    if (expected.bootstrap == BootstrapMode::SingleMessageMulti ||
6✔
392
                        expected.bootstrap == BootstrapMode::SingleMessage) {
6✔
393
                        REQUIRE(bootstrap_msg_count == 1);
4!
394
                    }
4✔
395
                    else if (expected.bootstrap == BootstrapMode::MultiMessage) {
2✔
396
                        REQUIRE(bootstrap_msg_count > 1);
2!
397
                    }
2✔
398
                    break;
6✔
399
            }
14✔
400
            return std::nullopt; // Don't transition
14✔
401
        });
14✔
402

403
        // Reset the state machine to "not ready" before leaving
404
        state_machina.transition_to(TestState::not_ready);
14✔
405
    };
14✔
406

407
    auto setup_test = [&](FLXSyncTestHarness& harness, nlohmann::json initial_rules, size_t initial_count) {
6✔
408
        // If an intial set of rules are provided, then set them now
409
        auto& app_session = harness.session().app_session();
6✔
410
        // If the rules are empty, then reset to the initial default state
411
        if (initial_rules.empty()) {
6✔
412
            initial_rules = app_session.admin_api.get_default_rule(app_session.server_app_id);
6✔
413
            AppCreateConfig::ServiceRole general_role{"default"};
6✔
414
            initial_rules["roles"] = {};
6✔
415
            initial_rules["roles"][0] = transform_service_role(general_role);
6✔
416
        }
6✔
417
        logger->debug("ROLE CHANGE: Initial rule definitions: %1", initial_rules);
6✔
418
        app_session.admin_api.update_default_rule(app_session.server_app_id, initial_rules);
6✔
419

420
        // Create and set up a new realm to be returned; wait for data sync
421
        auto config = harness.make_test_file();
6✔
422
        setup_config_callbacks(config);
6✔
423
        auto setup_realm = Realm::get_shared_realm(config);
6✔
424
        set_up_realm(setup_realm, initial_count);
6✔
425
        return setup_realm;
6✔
426
    };
6✔
427

428
    // 150 emps, 25 mgrs, 10 dirs
429
    // 10 objects before flush
430
    // 4096 download soft max bytes
431
    HarnessParams params{};
6✔
432

433
    // Only create the harness one time for all the sections under this test case
434
    static std::unique_ptr<FLXSyncTestHarness> harness;
6✔
435
    if (!harness) {
6✔
436
        harness = setup_harness("flx_role_change_bootstraps", params);
2✔
437
    }
2✔
438

439
    size_t num_total = params.num_emps + params.num_mgrs + params.num_dirs;
6✔
440
    auto realm_1 = setup_test(*harness, {}, num_total);
6✔
441
    // Get the current rules so it can be updated during the test
442
    auto& app_session = harness->session().app_session();
6✔
443
    auto test_rules = app_session.admin_api.get_default_rule(app_session.server_app_id);
6✔
444

445
    SECTION("Role changes lead to objects in/out of view without client reset") {
6✔
446
        // Single message bootstrap - remove employees, keep mgrs/dirs
447
        logger->trace("ROLE CHANGE: Updating rules to remove employees");
2✔
448
        update_role(test_rules, {{"role", {{"$in", {"manager", "director"}}}}});
2✔
449
        update_perms_and_verify(*harness, realm_1, test_rules,
2✔
450
                                {BootstrapMode::SingleMessage, 0, params.num_mgrs, params.num_dirs});
2✔
451
        // Write the same rules again - the client should not receive the reconnect (200) error
452
        logger->trace("ROLE CHANGE: Updating same rules again and verify reconnect doesn't happen");
2✔
453
        update_perms_and_verify(*harness, realm_1, test_rules,
2✔
454
                                {BootstrapMode::NoErrorNoBootstrap, 0, params.num_mgrs, params.num_dirs});
2✔
455
        // Multi-message bootstrap - add employeees, remove managers and directors
456
        logger->trace("ROLE CHANGE: Updating rules to add back the employees and remove mgrs/dirs");
2✔
457
        update_role(test_rules, {{"role", "employee"}});
2✔
458
        update_perms_and_verify(*harness, realm_1, test_rules, {BootstrapMode::MultiMessage, params.num_emps, 0, 0});
2✔
459
        // Single message/multi-changeset bootstrap - add back the managers and directors
460
        logger->trace("ROLE CHANGE: Updating rules to allow all records");
2✔
461
        update_role(test_rules, true);
2✔
462
        update_perms_and_verify(
2✔
463
            *harness, realm_1, test_rules,
2✔
464
            {BootstrapMode::SingleMessageMulti, params.num_emps, params.num_mgrs, params.num_dirs});
2✔
465
    }
2✔
466
    SECTION("Role changes for one user do not change unaffected user") {
6✔
467
        // Get the config for the first user
468
        auto config_1 = harness->make_test_file();
2✔
469

470
        // Start with a default rule that only allows access to the employee records
471
        AppCreateConfig::ServiceRole general_role{"default"};
2✔
472
        general_role.document_filters.read = {{"role", "employee"}};
2✔
473
        general_role.document_filters.write = {{"role", "employee"}};
2✔
474

475
        test_rules["roles"][0] = transform_service_role(general_role);
2✔
476
        harness->do_with_new_realm([&](SharedRealm new_realm) {
2✔
477
            set_up_realm(new_realm, num_total);
2✔
478

479
            // Add the initial rule and verify the data in realm 1 and 2 (both should just have the employees)
480
            update_perms_and_verify(*harness, realm_1, test_rules,
2✔
481
                                    {BootstrapMode::AnyBootstrap, params.num_emps, 0, 0});
2✔
482
            bool update_successful = wait_and_verify(new_realm, params.num_emps, 0, 0);
2✔
483
            REQUIRE(update_successful);
2!
484
        });
2✔
485
        {
2✔
486
            // Create another user and a new realm config for that user
487
            create_user_and_log_in(harness->app());
2✔
488
            auto config_2 = harness->make_test_file();
2✔
489
            REQUIRE(config_1.sync_config->user->user_id() != config_2.sync_config->user->user_id());
2!
490
            std::atomic<bool> test_started = false;
2✔
491

492
            // Reopen realm 2 and add a hook callback to check for bootstraps, which should not happen
493
            // on this realm
494
            config_2.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>,
2✔
495
                                                                  const SyncClientHookData& data) {
76✔
496
                using Event = SyncClientHookEvent;
76✔
497
                if (!test_started.load()) {
76✔
498
                    return SyncClientHookAction::NoAction; // Not checking yet
52✔
499
                }
52✔
500
                // If a download message was received or bootstrap was processed, then fail the test
501
                if ((data.event == Event::DownloadMessageReceived &&
24✔
502
                     data.batch_state != sync::DownloadBatchState::SteadyState) ||
24!
503
                    data.event == Event::BootstrapMessageProcessed || data.event == Event::BootstrapProcessed) {
24✔
504
                    FAIL("Bootstrap occurred on the second realm, which was not expected");
×
505
                }
×
506
                return SyncClientHookAction::NoAction;
24✔
507
            };
76✔
508
            auto realm_2 = Realm::get_shared_realm(config_2);
2✔
509
            set_up_realm(realm_2, params.num_emps);
2✔
510

511
            test_started = true;
2✔
512
            // The first rule allows access to all records for user 1
513
            AppCreateConfig::ServiceRole user1_role{"user 1 role"};
2✔
514
            user1_role.apply_when = {{"%%user.id", config_1.sync_config->user->user_id()}};
2✔
515
            // Add two rules, the first applies to user 1 and the second applies to other users
516
            test_rules["roles"] = {transform_service_role(user1_role), transform_service_role(general_role)};
2✔
517
            // Realm 1 should receive a role change bootstrap which updates the data to all records
518
            // It doesn't matter what type of bootstrap occurs
519
            update_perms_and_verify(*harness, realm_1, test_rules,
2✔
520
                                    {BootstrapMode::AnyBootstrap, params.num_emps, params.num_mgrs, params.num_dirs});
2✔
521

522
            // Realm 2 data should not change (and there shouldn't be any bootstrap messages)
523
            verify_records(realm_2, params.num_emps, 0, 0);
2✔
524

525
            // The first rule will be updated to only have access to employee and managers
526
            AppCreateConfig::ServiceRole user1_role_2 = user1_role;
2✔
527
            user1_role_2.document_filters.read = {{"role", {{"$in", {"employee", "manager"}}}}};
2✔
528
            user1_role_2.document_filters.write = {{"role", {{"$in", {"employee", "manager"}}}}};
2✔
529
            // Update the first rule for user 1 and verify the data after the rule is applied
530
            test_rules["roles"][0] = transform_service_role(user1_role_2);
2✔
531
            // Realm 1 should receive a role change bootstrap which updates the data to employee
532
            // and manager records. It doesn't matter what type of bootstrap occurs
533
            update_perms_and_verify(*harness, realm_1, test_rules,
2✔
534
                                    {BootstrapMode::AnyBootstrap, params.num_emps, params.num_mgrs, 0});
2✔
535

536
            // Realm 2 data should not change (and there shouldn't be any bootstrap messages)
537
            verify_records(realm_2, params.num_emps, 0, 0);
2✔
538
        }
2✔
539
    }
2✔
540

541
    // ----------------------------------------------------------------
542
    // Add new sections before this one
543
    // ----------------------------------------------------------------
544
    SECTION("Pending changes are lost if not allowed after role change") {
6✔
545
        std::vector<ObjectId> emp_ids;
2✔
546
        std::vector<ObjectId> mgr_ids;
2✔
547
        auto config = harness->make_test_file();
2✔
548
        config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
549
            REQUIRE(!error.is_fatal); // No fatal errors please
2!
550
            // Expecting a compensating write error
551
            REQUIRE(error.status == ErrorCodes::SyncCompensatingWrite);
2!
552
        };
2✔
553
        auto test_realm = Realm::get_shared_realm(config);
2✔
554
        set_up_realm(test_realm, num_total);
2✔
555
        // Perform the local updates offline
556
        test_realm->sync_session()->shutdown_and_wait();
2✔
557
        // Modify a set of records with new roles and create some new records as well
558
        // This should be called offline so the changes aren't sync'ed prematurely
559
        auto update_records = [](SharedRealm update_realm, std::string_view role_to_change,
2✔
560
                                 std::vector<ObjectId>& saved_ids, size_t num_to_modify, size_t num_to_create) {
4✔
561
            update_realm->begin_transaction();
4✔
562
            auto table = update_realm->read_group().get_table("class_Person");
4✔
563
            auto id_col = table->get_column_key("_id");
4✔
564
            auto role_col = table->get_column_key("role");
4✔
565
            auto name_col = table->get_column_key("name");
4✔
566
            auto empid_col = table->get_column_key("emp_id");
4✔
567
            auto table_query = Query(table).equal(role_col, role_to_change.data());
4✔
568
            auto results = Results(update_realm, table_query);
4✔
569
            REQUIRE(results.size() > 0);
4!
570
            // Modify the role of some existing objects
571
            for (size_t i = 0; i < num_to_modify; i++) {
34✔
572
                auto obj = results.get(i);
30✔
573
                saved_ids.push_back(obj.get<ObjectId>(id_col));
30✔
574
                obj.set(role_col, "worker-bee");
30✔
575
            }
30✔
576
            // And create some new objects
577
            for (size_t i = 0; i < num_to_create; i++) {
24✔
578
                auto obj = table->create_object_with_primary_key(ObjectId::gen());
20✔
579
                obj.set(role_col, role_to_change.data());
20✔
580
                obj.set(name_col, util::format("%1-%2(new)", role_to_change.data(), i));
20✔
581
                obj.set(empid_col, static_cast<int64_t>(i + 2500)); // actual # doesnt matter
20✔
582
            }
20✔
583
            update_realm->commit_transaction();
4✔
584
        };
4✔
585
        auto do_update_rules = [&](nlohmann::json new_rules) {
4✔
586
            update_role(test_rules, new_rules);
4✔
587
            logger->debug("ROLE CHANGE: Updating rule definitions: %1", test_rules);
4✔
588
            app_session.admin_api.update_default_rule(app_session.server_app_id, test_rules);
4✔
589
        };
4✔
590
        auto do_verify = [](SharedRealm realm, size_t cnt, std::vector<ObjectId>& saved_ids,
2✔
591
                            std::optional<std::string_view> expected = std::nullopt) {
6✔
592
            REQUIRE(!wait_for_download(*realm));
6!
593
            REQUIRE(!wait_for_upload(*realm));
6!
594
            wait_for_advance(*realm);
6✔
595
            // Verify none of the records modified above exist in the realm
596
            auto table = realm->read_group().get_table("class_Person");
6✔
597
            REQUIRE(table->size() == cnt);
6!
598
            auto id_col = table->get_column_key("_id");
6✔
599
            auto role_col = table->get_column_key("role");
6✔
600
            for (auto& id : saved_ids) {
50✔
601
                auto objkey = table->find_first(id_col, id);
50✔
602
                if (expected) {
50✔
603
                    REQUIRE(objkey);
30!
604
                    auto obj = table->get_object(objkey);
30✔
605
                    REQUIRE(obj.get<String>(role_col) == *expected);
30!
606
                }
30✔
607
                else {
20✔
608
                    REQUIRE(!objkey);
20!
609
                }
20✔
610
            }
50✔
611
        };
6✔
612
        // Update the rules so employees are not allowed and removed from view
613
        // This will also remove the existing changes to the 10 employee records
614
        // and the 5 new employee records.
615
        size_t num_to_create = 5;
2✔
616
        // Update 10 employees to worker-bee and create 5 new employees
617
        update_records(test_realm, "employee", emp_ids, 10, num_to_create);
2✔
618
        // Update 5 managers to worker-bee and create 5 new managers
619
        update_records(test_realm, "manager", mgr_ids, 5, num_to_create);
2✔
620
        // Update the allowed roles to "manager" and "worker-bee"
621
        do_update_rules({{"role", {{"$in", {"manager", "worker-bee"}}}}});
2✔
622
        // Resume the session and verify none of the new/modified employee
623
        // records are present
624
        test_realm->sync_session()->resume();
2✔
625
        // Verify none of the employee object IDs are present in the local data
626
        do_verify(test_realm, params.num_mgrs + num_to_create, emp_ids, std::nullopt);
2✔
627
        // Verify all of the manager object IDs are present in the local data
628
        do_verify(test_realm, params.num_mgrs + num_to_create, mgr_ids, "worker-bee");
2✔
629

630
        // Update the allowed roles to "employee"
631
        do_update_rules({{"role", "employee"}});
2✔
632
        // Verify the items with the object IDs are still listed as employees
633
        do_verify(test_realm, params.num_emps, emp_ids, "employee");
2✔
634

635
        // Tear down the app since some of the records were added and modified
636
        harness.reset();
2✔
637
    }
2✔
638
}
6✔
639

640
TEST_CASE("flx: role changes during bootstrap complete successfully", "[sync][flx][baas][role change][bootstrap]") {
20✔
641
    auto logger = util::Logger::get_default_logger();
20✔
642

643
    // 150 emps, 25 mgrs, 10 dirs
644
    // 10 objects before flush
645
    // 1536 download soft max bytes
646
    HarnessParams params{};
20✔
647
    params.max_download_bytes = 1536; // 1.5 KB
20✔
648

649
    // Only create the harness one time for all the sections under this test case
650
    static std::unique_ptr<FLXSyncTestHarness> harness;
20✔
651
    if (!harness) {
20✔
652
        harness = setup_harness("flx_role_change_during_bs", params);
2✔
653
    }
2✔
654

655
    // Get the current rules so it can be updated during the test
656
    auto& app_session = harness->session().app_session();
20✔
657
    auto default_rule = app_session.admin_api.get_default_rule(app_session.server_app_id);
20✔
658

659
    // Make sure the rules are reset back to the original value (all records allowed)
660
    update_role(default_rule, true);
20✔
661
    logger->debug("ROLE CHANGE: Initial rule definitions: %1", default_rule);
20✔
662
    REQUIRE(app_session.admin_api.update_default_rule(app_session.server_app_id, default_rule));
20!
663

664
    enum BootstrapTestState {
20✔
665
        not_ready,
20✔
666
        start,
20✔
667
        ident_sent,
20✔
668
        reconnect_received,
20✔
669
        downloading,
20✔
670
        downloaded,
20✔
671
        integrating,
20✔
672
        integration_complete,
20✔
673
        complete
20✔
674
    };
20✔
675

676
    BootstrapTestState update_role_state = BootstrapTestState::not_ready;
20✔
677
    int update_msg_count = -1;
20✔
678
    int bootstrap_count = 0;
20✔
679
    int bootstrap_msg_count = 0;
20✔
680
    bool session_restarted = false;
20✔
681
    TestingStateMachine<BootstrapTestState> bootstrap_state(BootstrapTestState::not_ready);
20✔
682

683
    auto setup_config_callbacks = [&](SyncTestFile& config) {
20✔
684
        // Use the sync client event hook to check for the error received and for tracking
685
        // download messages and bootstraps
686
        config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>,
20✔
687
                                                            const SyncClientHookData& data) {
888✔
688
            bootstrap_state.transition_with([&](BootstrapTestState cur_state) -> std::optional<BootstrapTestState> {
888✔
689
                using BatchState = sync::DownloadBatchState;
888✔
690
                using Event = SyncClientHookEvent;
888✔
691
                // Keep track of the number of bootstraps that have occurred, regardless of cur state
692
                if (data.event == Event::BootstrapProcessed) {
888✔
693
                    bootstrap_count++;
50✔
694
                }
50✔
695

696
                // Has the test started?
697
                if (cur_state == BootstrapTestState::not_ready)
888✔
698
                    return std::nullopt;
208✔
699

700
                std::optional<BootstrapTestState> new_state;
680✔
701

702
                switch (data.event) {
680✔
703
                    case Event::IdentMessageSent:
34✔
704
                        new_state = BootstrapTestState::ident_sent;
34✔
705
                        break;
34✔
706

707
                    case Event::ErrorMessageReceived:
16✔
708
                        REQUIRE(data.error_info->raw_error_code ==
16!
709
                                static_cast<int>(sync::ProtocolError::session_closed));
16✔
710
                        REQUIRE(data.error_info->server_requests_action ==
16!
711
                                sync::ProtocolErrorInfo::Action::Transient);
16✔
712
                        REQUIRE_FALSE(data.error_info->is_fatal);
16!
713
                        session_restarted = true;
16✔
714
                        break;
16✔
715

716
                    // A bootstrap message was processed
717
                    case Event::BootstrapMessageProcessed:
170✔
718
                        bootstrap_msg_count++;
170✔
719
                        if (data.batch_state == BatchState::LastInBatch) {
170✔
720
                            new_state = BootstrapTestState::downloaded;
34✔
721
                        }
34✔
722
                        else if (data.batch_state == BatchState::MoreToCome) {
136✔
723
                            new_state = BootstrapTestState::downloading;
136✔
724
                        }
136✔
725
                        break;
170✔
726

727
                    case SyncClientHookEvent::DownloadMessageIntegrated:
44✔
728
                        if (data.batch_state == BatchState::SteadyState) {
44✔
729
                            break;
10✔
730
                        }
10✔
731
                        REQUIRE((cur_state == BootstrapTestState::downloaded ||
34!
732
                                 cur_state == BootstrapTestState::integrating));
34✔
733
                        new_state = BootstrapTestState::integrating;
34✔
734
                        break;
34✔
735

736
                    // The bootstrap has been received and processed
737
                    case Event::BootstrapProcessed:
34✔
738
                        REQUIRE(cur_state == BootstrapTestState::integrating);
34!
739
                        new_state = BootstrapTestState::integration_complete;
34✔
740
                        break;
34✔
741

742
                    default:
382✔
743
                        break;
382✔
744
                }
680✔
745
                // If the state is changing and a role change is requested for that state, then
746
                // update the role now.
747
                if (new_state && new_state == update_role_state &&
680✔
748
                    update_role_state != BootstrapTestState::not_ready && bootstrap_msg_count >= update_msg_count) {
680✔
749
                    logger->debug("ROLE CHANGE: Updating rule definitions: %1", default_rule);
18✔
750
                    REQUIRE(app_session.admin_api.update_default_rule(app_session.server_app_id, default_rule));
18!
751
                    update_role_state = BootstrapTestState::not_ready; // Bootstrap tracking is complete
18✔
752
                }
18✔
753
                return new_state;
680✔
754
            });
680✔
755
            return SyncClientHookAction::NoAction;
888✔
756
        };
888✔
757

758
        // Add client reset callback to verify a client reset doesn't happen
759
        config.sync_config->notify_before_client_reset = [&](std::shared_ptr<Realm>) {
20✔
760
            // Make sure a client reset did not occur while waiting for the role change to
761
            // be applied
762
            FAIL("Client reset is not expected when the role/rules/permissions are changed");
×
763
        };
×
764
    };
20✔
765

766
    auto setup_test_params = [&](BootstrapTestState change_state, int msg_count = -1) {
20✔
767
        // Use the state machine mutex to protect the variables shared with the event hook
768
        bootstrap_state.transition_with([&](BootstrapTestState) {
18✔
769
            bootstrap_count = 0;              // Reset the bootstrap count
18✔
770
            bootstrap_msg_count = 0;          // Reset the bootstrap msg count
18✔
771
            update_role_state = change_state; // State where the role change should be sent
18✔
772
            update_msg_count = msg_count;     // Wait for this many download messages
18✔
773
            return BootstrapTestState::start; // Update to start to begin tracking state
18✔
774
        });
18✔
775
    };
18✔
776

777
    // Create the shared realm and configure a subscription for the manager and director records
778
    auto config = harness->make_test_file();
20✔
779
    setup_config_callbacks(config);
20✔
780

781
    SECTION("Role change during initial schema bootstrap") {
20✔
782
        // Trigger the role change after the IDENT message is sent so the role change
783
        // bootstrap will occur while the new realm is receiving the schema bootstrap
784
        setup_test_params(BootstrapTestState::ident_sent);
2✔
785
        auto realm_1 = Realm::get_shared_realm(config);
2✔
786
        REQUIRE(!wait_for_download(*realm_1));
2!
787
        REQUIRE(!wait_for_upload(*realm_1));
2!
788
        // Use the state machine mutex to protect the variables shared with the event hook
789
        bootstrap_state.transition_with([&](BootstrapTestState) {
2✔
790
            // Only the initial schema bootstrap with 1 download message should take place
791
            // without restarting the session
792
            REQUIRE(bootstrap_count == 1);
2!
793
            REQUIRE(bootstrap_msg_count == 1);
2!
794
            // Bootstrap was not triggered, since it's a new file ident
795
            REQUIRE_FALSE(session_restarted);
2!
796
            return std::nullopt;
2✔
797
        });
2✔
798
    }
2✔
799
    SECTION("Role change during subscription bootstrap") {
20✔
800
        auto realm_1 = Realm::get_shared_realm(config);
16✔
801
        bool initial_subscription = GENERATE(false, true);
16✔
802

803
        if (initial_subscription) {
16✔
804
            auto table = realm_1->read_group().get_table("class_Person");
8✔
805
            auto role_col = table->get_column_key("role");
8✔
806
            auto sub_query = Query(table).equal(role_col, "manager").Or().equal(role_col, "director");
8✔
807
            auto new_subs = realm_1->get_latest_subscription_set().make_mutable_copy();
8✔
808
            new_subs.insert_or_assign(sub_query);
8✔
809
            auto subs = new_subs.commit();
8✔
810

811
            // Wait for subscription bootstrap to and sync to complete
812
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
8✔
813

814
            // Verify the data was downloaded and only includes managers and directors
815
            bool update_successful = wait_and_verify(realm_1, 0, params.num_mgrs, params.num_dirs);
8✔
816
            REQUIRE(update_successful);
8!
817
        }
8✔
818

819
        // The test will update the rule to change access from all records to only the employee
820
        // records while a new subscription for all Person entries is being bootstrapped.
821
        update_role(default_rule, {{"role", "employee"}});
16✔
822

823
        // Set up a new bootstrap while offline
824
        realm_1->sync_session()->shutdown_and_wait();
16✔
825
        {
16✔
826
            // Set up a subscription for the Person table
827
            auto table = realm_1->read_group().get_table("class_Person");
16✔
828
            auto new_subs = realm_1->get_latest_subscription_set().make_mutable_copy();
16✔
829
            new_subs.clear();
16✔
830
            new_subs.insert_or_assign(Query(table));
16✔
831
            auto subs = new_subs.commit();
16✔
832
            // Each one of these sections runs the role change bootstrap test with different
833
            // settings for the `update_role_state` which indicates at which stage during
834
            // the bootstrap where the role change will occur.
835
            SECTION("Role change occurs during bootstrap download") {
16✔
836
                logger->debug("ROLE CHANGE: Role change during %1 query bootstrap download",
4✔
837
                              initial_subscription ? "second" : "first");
4✔
838
                // Wait for the downloading state and 3 messages have been downloaded
839
                setup_test_params(BootstrapTestState::downloading, 3);
4✔
840
            }
4✔
841
            SECTION("Role change occurs after bootstrap downloaded") {
16✔
842
                logger->debug("ROLE CHANGE: Role change after %1 query bootstrap download",
4✔
843
                              initial_subscription ? "second" : "first");
4✔
844
                // Wait for the downloaded state
845
                setup_test_params(BootstrapTestState::downloaded);
4✔
846
            }
4✔
847
            SECTION("Role change occurs during bootstrap integration") {
16✔
848
                logger->debug("ROLE CHANGE: Role change during %1 query bootstrap integration",
4✔
849
                              initial_subscription ? "second" : "first");
4✔
850
                // Wait for bootstrap messages to be integrated
851
                setup_test_params(BootstrapTestState::integrating);
4✔
852
            }
4✔
853
            SECTION("Role change occurs after bootstrap integration") {
16✔
854
                logger->debug("ROLE CHANGE: Role change after %1 query bootstrap integration",
4✔
855
                              initial_subscription ? "second" : "first");
4✔
856
                // Wait for the end of the bootstrap integration
857
                setup_test_params(BootstrapTestState::integration_complete);
4✔
858
            }
4✔
859

860
            // Resume the session an wait for subscription bootstrap to and sync to complete
861
            realm_1->sync_session()->resume();
16✔
862
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
16✔
863

864
            // Verify the data was downloaded/updated (only the employee records)
865
            bool update_successful = wait_and_verify(realm_1, params.num_emps, 0, 0);
16✔
866

867
            // Use the state machine mutex to protect the variables shared with the event hook
868
            bootstrap_state.transition_with([&](BootstrapTestState) {
16✔
869
                if (!update_successful) {
16✔
870
                    FAIL("Failed to wait for realm update during role change bootstrap");
×
871
                }
×
872
                // Expecting two bootstraps have occurred (role change and subscription)
873
                // and the session was restarted with 200 error.
874
                REQUIRE(session_restarted);
16!
875
                REQUIRE(bootstrap_count == 2);
16!
876
                REQUIRE(bootstrap_msg_count > 1);
16!
877
                return std::nullopt;
16✔
878
            });
16✔
879
        }
16✔
880
    }
16✔
881
    // ----------------------------------------------------------------
882
    // Add new sections before this one
883
    // ----------------------------------------------------------------
884
    SECTION("teardown") {
20✔
885
        // Since the harness is reused for each of the role change during bootstrap tests,
886
        // this section will run last to destroy the harness after the tests are complete.
887
        harness.reset();
2✔
888
    }
2✔
889
}
20✔
890

891
TEST_CASE("flx: role changes during client resets complete successfully",
892
          "[sync][flx][baas][role change][client reset]") {
20✔
893
    auto logger = util::Logger::get_default_logger();
20✔
894

895
    // 150 emps, 25 mgrs, 25 dirs
896
    // 10 objects before flush
897
    // 512 download soft max bytes
898
    HarnessParams params{};
20✔
899
    params.num_dirs = 25;
20✔
900
    params.max_download_bytes = 512;
20✔
901

902
    // Only create the harness one time for all the sections under this test case
903
    static std::unique_ptr<FLXSyncTestHarness> harness;
20✔
904
    if (!harness) {
20✔
905
        harness = setup_harness("flx_role_change_during_cr", params);
2✔
906
    }
2✔
907

908
    SECTION("Role change during client reset") {
20✔
909
        // Get the current rules so it can be updated during the test
910
        auto& app_session = harness->session().app_session();
18✔
911
        auto default_rule = app_session.admin_api.get_default_rule(app_session.server_app_id);
18✔
912

913
        enum ClientResetTestState {
18✔
914
            not_ready,
18✔
915
            start,
18✔
916
            // Primary sync session states before client reset
917
            bind_before_cr_session,
18✔
918
            // Fresh realm download sync session states
919
            cr_session_ident,
18✔
920
            cr_session_downloading,
18✔
921
            cr_session_downloaded,
18✔
922
            cr_session_integrating,
18✔
923
            cr_session_integrated,
18✔
924
            // Primary sync session states after fresh realm download
925
            bind_after_cr_session,
18✔
926
            merged_after_cr_session,
18✔
927
            ident_after_cr_session,
18✔
928
        };
18✔
929

930
        bool client_reset_error = false;
18✔
931
        bool role_change_error = false;
18✔
932
        ClientResetTestState update_role_state = ClientResetTestState::not_ready;
18✔
933
        int client_reset_count = 0;
18✔
934
        bool skip_role_change_check = false;
18✔
935
        TestingStateMachine<ClientResetTestState> client_reset_state(ClientResetTestState::not_ready);
18✔
936

937
        // Set the state where the role change will be triggered
938
        constexpr bool skip_role_change_error_check = true;
18✔
939
        auto setup_test_params = [&](ClientResetTestState change_state, bool skip_role_error = false) {
18✔
940
            client_reset_state.transition_with([&](ClientResetTestState) {
18✔
941
                client_reset_error = false;       // Reset the client reset error tracking
18✔
942
                role_change_error = false;        // Reset the role change error tracking
18✔
943
                client_reset_count = 0;           // Reset the client reset error count
18✔
944
                update_role_state = change_state; // State where the role change should be sent
18✔
945
                // If the role change check is skipped, the test will not look for the role change error
946
                // Depending on when the role change error is received (e.g. session deactivating), it
947
                // may not be successfully or reliably captured with the event hook.
948
                skip_role_change_check = skip_role_error;
18✔
949
                return ClientResetTestState::start; // Update to start to begin tracking state
18✔
950
            });
18✔
951
        };
18✔
952

953
        auto setup_config_callbacks = [&](SyncTestFile& config) {
18✔
954
            // Use the sync client event hook to check for the error received and for tracking
955
            // download messages and bootstraps
956
            config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
18✔
957
                                                                const SyncClientHookData& data) {
1,707✔
958
                bool is_fresh_path;
1,707✔
959
                auto session = weak_session.lock();
1,707✔
960
                if (!session) {
1,707✔
961
                    // Session is being closed while this function was called
NEW
962
                    return SyncClientHookAction::NoAction;
×
NEW
963
                }
×
964
                is_fresh_path = _impl::client_reset::is_fresh_path(session->path());
1,707✔
965

966
                client_reset_state.transition_with(
1,707✔
967
                    [&](ClientResetTestState cur_state) -> std::optional<ClientResetTestState> {
1,707✔
968
                        using BatchState = sync::DownloadBatchState;
1,707✔
969
                        using Event = SyncClientHookEvent;
1,707✔
970

971
                        // Exit early if the test/state tracking hasn't started
972
                        if (cur_state == ClientResetTestState::not_ready)
1,707✔
973
                            return std::nullopt;
540✔
974

975
                        // If an error occurred, check to see if it is a client reset error or the
976
                        // session restart (due to the role change).
977
                        if (data.event == Event::ErrorMessageReceived) {
1,167✔
978
                            REQUIRE(data.error_info);
32!
979
                            // Client reset error occurred
980
                            if (data.error_info->raw_error_code ==
32✔
981
                                static_cast<int>(sync::ProtocolError::bad_client_file_ident)) {
32✔
982
                                REQUIRE(data.error_info->should_client_reset);
18!
983
                                REQUIRE(data.error_info->server_requests_action ==
18!
984
                                        sync::ProtocolErrorInfo::Action::ClientReset);
18✔
985
                                REQUIRE(data.error_info->is_fatal);
18!
986
                                logger->debug("ROLE CHANGE: client reset error received");
18✔
987
                                client_reset_error = true;
18✔
988
                            }
18✔
989
                            // 200 error is received to start role change bootstrap
990
                            else if (data.error_info->raw_error_code ==
14✔
991
                                     static_cast<int>(sync::ProtocolError::session_closed)) {
14✔
992
                                REQUIRE(data.error_info->server_requests_action ==
14!
993
                                        sync::ProtocolErrorInfo::Action::Transient);
14✔
994
                                REQUIRE_FALSE(data.error_info->is_fatal);
14!
995
                                logger->debug("ROLE CHANGE: role change error received");
14✔
996
                                role_change_error = true;
14✔
997
                            }
14✔
998
                            // Other errors are not expected
999
                            else {
×
1000
                                FAIL(util::format("Unexpected %1 error occurred during role change test: [%2] %3",
×
1001
                                                  data.error_info->is_fatal ? "fatal" : "non-fatal",
×
1002
                                                  data.error_info->raw_error_code, data.error_info->message));
×
1003
                            }
×
1004
                            return std::nullopt;
32✔
1005
                        }
32✔
1006
                        std::optional<ClientResetTestState> new_state = std::nullopt;
1,135✔
1007
                        // Once the client reset progresses to the state that matches the `update_role_state`
1008
                        // value, the role change will occur and `update_role_state` will be cleared.
1009
                        if (update_role_state == ClientResetTestState::not_ready) {
1,135✔
1010
                            // Once update_role_state is cleared, tracking the state is no longer necessary
1011
                            return std::nullopt;
805✔
1012
                        }
805✔
1013
                        // Track the state of the client reset progress, from receiving the client reset error,
1014
                        // to downloading the fresh realm, to the client reset diff when the primary session
1015
                        // restarts. The state is used to kick off the role change when the client reset state
1016
                        // reaches the state specified by `update_role_state`. Once the role change has been
1017
                        // initiated, `update_role_state` will be cleared and the state will no longer be
1018
                        // tracked for the rest of the test (other than looking for the errors above).
1019
                        switch (data.event) {
330✔
1020
                            case Event::BindMessageSent:
40✔
1021
                                // "bind_before_cr_session" - BIND msg sent prior to receiving client reset error
1022
                                if (cur_state == ClientResetTestState::start) {
40✔
1023
                                    REQUIRE_FALSE(client_reset_error);
18!
1024
                                    new_state = ClientResetTestState::bind_before_cr_session;
18✔
1025
                                }
18✔
1026
                                // "bind_after_cr_session" - BIND msg sent after fresh realm download session is
1027
                                // complete
1028
                                else if (cur_state == ClientResetTestState::cr_session_integrated) {
22✔
1029
                                    REQUIRE(client_reset_error);
6!
1030
                                    new_state = ClientResetTestState::bind_after_cr_session;
6✔
1031
                                }
6✔
1032
                                break;
40✔
1033
                            case Event::ClientResetMergeComplete:
40✔
1034
                                // "merged_after_cr_session" - client reset diff is complete
1035
                                REQUIRE(cur_state == ClientResetTestState::bind_after_cr_session);
4!
1036
                                REQUIRE_FALSE(is_fresh_path);
4!
1037
                                REQUIRE(client_reset_error);
4!
1038
                                new_state = ClientResetTestState::merged_after_cr_session;
4✔
1039
                                break;
4✔
1040
                            case Event::IdentMessageSent:
34✔
1041
                                // Skip the IDENT message if the client reset error hasn't occurred
1042
                                if (!client_reset_error)
34✔
1043
                                    break;
16✔
1044
                                // "cr_session_ident" - IDENT msg sent for the fresh realm download session
1045
                                if (cur_state == ClientResetTestState::bind_before_cr_session) {
18✔
1046
                                    REQUIRE(is_fresh_path);
16!
1047
                                    new_state = ClientResetTestState::cr_session_ident;
16✔
1048
                                }
16✔
1049
                                // "ident_after_cr_session" - IDENT msg sent after client reset diff is complete
1050
                                else if (cur_state == ClientResetTestState::merged_after_cr_session) {
2✔
1051
                                    REQUIRE_FALSE(is_fresh_path);
2!
1052
                                    new_state = ClientResetTestState::ident_after_cr_session;
2✔
1053
                                }
2✔
1054
                                break;
18✔
1055
                            // A bootstrap message was processed by the client reset session
1056
                            case Event::BootstrapMessageProcessed:
52✔
1057
                                // "cr_session_downloaded" - last DOWNLOAD message received of fresh realm bootstrap
1058
                                if (!client_reset_error || data.batch_state == BatchState::SteadyState)
52✔
1059
                                    break;
×
1060
                                if (data.batch_state == BatchState::LastInBatch) {
52✔
1061
                                    new_state = ClientResetTestState::cr_session_downloaded;
20✔
1062
                                }
20✔
1063
                                // "cr_session_downloading" - first DOWNLOAD message received of fresh realm bootstrap
1064
                                else if (data.batch_state == BatchState::MoreToCome) {
32✔
1065
                                    new_state = ClientResetTestState::cr_session_downloading;
32✔
1066
                                }
32✔
1067
                                break;
52✔
1068
                            case Event::DownloadMessageIntegrated:
18✔
1069
                                if (!client_reset_error)
18✔
1070
                                    break;
×
1071
                                // "cr_session_integrating" - fresh realm bootstrap is being integrated
1072
                                new_state = ClientResetTestState::cr_session_integrating;
18✔
1073
                                break;
18✔
1074
                            // The client reset session has processed the bootstrap
1075
                            case Event::BootstrapProcessed:
16✔
1076
                                if (!client_reset_error)
16✔
1077
                                    break;
×
1078
                                // "cr_session_integrating" - fresh realm bootstrap integration is complete
1079
                                new_state = ClientResetTestState::cr_session_integrated;
16✔
1080
                                break;
16✔
1081
                            default:
166✔
1082
                                break;
166✔
1083
                        }
330✔
1084

1085
                        // If a new state is specified, check to see if it matches the value of `update_role_state`
1086
                        // and perform the role change if the two match. Once the role change has been sent, clear
1087
                        // `update_role_state` since the state doesn't need to be tracked anymore.
1088
                        if (new_state && update_role_state && *new_state == update_role_state) {
330✔
1089
                            logger->debug("ROLE CHANGE: Updating rule definitions: %1", default_rule);
18✔
1090
                            REQUIRE(
18!
1091
                                app_session.admin_api.update_default_rule(app_session.server_app_id, default_rule));
18✔
1092
                            update_role_state = ClientResetTestState::not_ready; // Bootstrap tracking is complete
18✔
1093
                        }
18✔
1094
                        return new_state;
330✔
1095
                    });
330✔
1096
                return SyncClientHookAction::NoAction;
1,707✔
1097
            };
1,707✔
1098

1099
            // Add client reset callback to count the number of times a client reset occurred (should be 1)
1100
            config.sync_config->notify_before_client_reset = [&](std::shared_ptr<Realm>) {
18✔
1101
                client_reset_state.transition_with([&](ClientResetTestState) {
18✔
1102
                    // Save that a client reset took place
1103
                    client_reset_count++;
18✔
1104
                    return std::nullopt;
18✔
1105
                });
18✔
1106
            };
18✔
1107

1108
            config.sync_config->error_handler = [](std::shared_ptr<SyncSession>, SyncError error) {
18✔
1109
                // Only expecting a client reset error to be reported
1110
                if (error.status != ErrorCodes::SyncClientResetRequired)
×
1111
                    FAIL(util::format("Unexpected error received by error handler: %1", error.status));
×
1112
            };
×
1113
        };
18✔
1114

1115
        // Start with the role/rules set to only allow manager and director records
1116
        update_role(default_rule, {{"role", {{"$in", {"manager", "director"}}}}});
18✔
1117
        logger->debug("ROLE CHANGE: Initial rule definitions: %1", default_rule);
18✔
1118
        REQUIRE(app_session.admin_api.update_default_rule(app_session.server_app_id, default_rule));
18!
1119

1120
        auto config_1 = harness->make_test_file();
18✔
1121
        auto&& [reset_future, reset_handler] = reset_utils::make_client_reset_handler();
18✔
1122
        config_1.sync_config->notify_after_client_reset = std::move(reset_handler);
18✔
1123
        config_1.sync_config->client_resync_mode = ClientResyncMode::Recover;
18✔
1124
        setup_config_callbacks(config_1);
18✔
1125

1126
        auto realm_1 = Realm::get_shared_realm(config_1);
18✔
1127
        {
18✔
1128
            // Set up a default subscription for all records of the Person class
1129
            auto table = realm_1->read_group().get_table("class_Person");
18✔
1130
            auto new_subs = realm_1->get_latest_subscription_set().make_mutable_copy();
18✔
1131
            new_subs.clear();
18✔
1132
            new_subs.insert_or_assign(Query(table));
18✔
1133
            auto subs = new_subs.commit();
18✔
1134
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
18✔
1135
            bool update_successful = wait_and_verify(realm_1, 0, params.num_mgrs, params.num_dirs);
18✔
1136
            REQUIRE(update_successful);
18!
1137
        }
18✔
1138
        // During the test, the role change will be updated from only allowing manager and director
1139
        // records to only allowing employee records while a client reset is in progress.
1140
        update_role(default_rule, {{"role", "employee"}});
18✔
1141
        // Force a client reset to occur the next time the session connects
1142
        reset_utils::trigger_client_reset(app_session, realm_1);
18✔
1143

1144
        // Each one of these sections runs the role change client reset test with the different
1145
        // setting for the `update_role_state` which indicates which stage during the client reset
1146
        // where the role change will occur. The verification of the session restart error (200)
1147
        // is difficult to catch at some stages, and the test will skip the 200 error verification.
1148
        SECTION("Role change occurs as soon as the BIND message is sent just before the client reset is started") {
18✔
1149
            logger->debug("ROLE CHANGE: Role change occurs as soon as the BIND message is sent just before the "
2✔
1150
                          "client reset is started");
2✔
1151
            setup_test_params(ClientResetTestState::bind_before_cr_session, skip_role_change_error_check);
2✔
1152
        }
2✔
1153
        SECTION("Role change occurs after the IDENT message is sent for the fresh realm download session") {
18✔
1154
            logger->debug("ROLE CHANGE: Role change occurs after the IDENT message is sent for the fresh realm "
2✔
1155
                          "download session");
2✔
1156
            setup_test_params(ClientResetTestState::cr_session_ident);
2✔
1157
        }
2✔
1158
        SECTION("Role change occurs while the fresh realm bootstrap is being downloaded") {
18✔
1159
            logger->debug("ROLE CHANGE: Role change occurs while the fresh realm bootstrap is being downloaded");
2✔
1160
            setup_test_params(ClientResetTestState::cr_session_downloading);
2✔
1161
        }
2✔
1162
        SECTION("Role change occurs as soon as the fresh realm bootstrap download is complete") {
18✔
1163
            logger->debug(
2✔
1164
                "ROLE CHANGE: Role change occurs as soon as the fresh realm bootstrap download is complete");
2✔
1165
            setup_test_params(ClientResetTestState::cr_session_downloaded);
2✔
1166
        }
2✔
1167
        SECTION("Role change occurs while the fresh realm bootstrap is being integrated") {
18✔
1168
            logger->debug("ROLE CHANGE: Role change occurs while the fresh realm bootstrap is being integrated");
2✔
1169
            // Trigger the role change while the subscription bootstrap changeset
1170
            // integration for the fresh realm sync session is in progress
1171
            setup_test_params(ClientResetTestState::cr_session_integrating);
2✔
1172
        }
2✔
1173
        SECTION("Role change occurs after fresh realm bootstrap is integrated") {
18✔
1174
            logger->debug("ROLE CHANGE: Role change occurs after fresh realm bootstrap is integrated");
2✔
1175
            setup_test_params(ClientResetTestState::cr_session_integrated);
2✔
1176
        }
2✔
1177
        SECTION("Role change occurs after BIND message is sent for the primary sync session") {
18✔
1178
            logger->debug("ROLE CHANGE: Role change occurs after BIND message is sent for the primary sync session");
2✔
1179
            setup_test_params(ClientResetTestState::bind_after_cr_session, skip_role_change_error_check);
2✔
1180
        }
2✔
1181
        SECTION("Role change occurs as soon as the client reset diff is complete") {
18✔
1182
            logger->debug("ROLE CHANGE: Role change occurs as soon as the client reset diff is complete");
2✔
1183
            setup_test_params(ClientResetTestState::merged_after_cr_session, skip_role_change_error_check);
2✔
1184
        }
2✔
1185
        SECTION("Role change occurs after IDENT message is sent after client reset merge is complete") {
18✔
1186
            logger->debug(
2✔
1187
                "ROLE CHANGE: Role change occurs after IDENT message is sent after client reset merge is complete");
2✔
1188
            // Trigger role change
1189
            // Trigger the role change after the IDENT message is sent after the client reset
1190
            // and before the MARK response is received from the server to close out the
1191
            // client reset.
1192
            setup_test_params(ClientResetTestState::ident_after_cr_session);
2✔
1193
        }
2✔
1194

1195
        // Client reset will happen when session tries to reconnect
1196
        realm_1->sync_session()->restart_session();
18✔
1197
        auto resync_mode = wait_for_future(std::move(reset_future)).get();
18✔
1198

1199
        // Verify the data was downloaded/updated (only the employee records)
1200
        bool update_successful = wait_and_verify(realm_1, params.num_emps, 0, 0);
18✔
1201

1202
        client_reset_state.transition_with([&](ClientResetTestState) {
18✔
1203
            if (!update_successful) {
18✔
1204
                FAIL("Failed to wait for realm update during role change bootstrap");
×
1205
            }
×
1206
            // Using the state machine mutex to protect the event hook shared variables
1207
            // Verify that the client reset occurred
1208
            REQUIRE(resync_mode == ClientResyncMode::Recover);
18!
1209
            REQUIRE(client_reset_error);
18!
1210
            REQUIRE(client_reset_count == 1);
18!
1211
            // Unless skip_role_change_check is set, verify role change error occurred as well
1212
            REQUIRE((role_change_error || skip_role_change_check));
18!
1213
            return std::nullopt;
18✔
1214
        });
18✔
1215
    }
18✔
1216
    // ----------------------------------------------------------------
1217
    // Add new sections before this one
1218
    // ----------------------------------------------------------------
1219
    SECTION("teardown") {
20✔
1220
        // Since the harness is reused for each of the role change client reset tests, this
1221
        // section will run last to destroy the harness after the tests are complete.
1222
        harness.reset();
2✔
1223
    }
2✔
1224
}
20✔
1225

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

© 2026 Coveralls, Inc