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

realm / realm-core / 2503

19 Jul 2024 12:39AM UTC coverage: 90.996%. Remained the same
2503

push

Evergreen

web-flow
Merge pull request #7897 from realm/feature/role-change

* RCORE-1872 Sync client should allow server bootstrapping at any time (#7440)
* First round of changes for server-initiated bootstraps
* Added test for role change bootstraps
* Updated test for handle role bootstraps
* Updated baas/baasaas to use branch with fixes
* Updated test to verify bootstrap actually occurred
* Fixed tsan warning
* Updates from review; added comments to clarify bootstrap detection logic
* Reverted baas branch to master and protocol version to 12
* Added comments to changes needed when merging to master; update baas version to not use master
* Pulled over changes from other branch and tweaking download params
* Refactored tests to validate different bootstrap types
* Updated tests to get passing using the server params
* Updated to support new batch_state protocol changes; updated tests
* Updated role change tests and merged test from separate PR
* Fixed issue with flx query verion 0 not being treated as a bootstrap
* Cleaned up the tests a bit and reworked query version 0 handling
* Updates from review; updated batch_state for schema bootstraps
* Removed extra mutex in favor of state machine's mutex
* Increased timeout when waiting for app initial sync to complete
* Updated role change test to use test commands
* Update resume and ident message handling
* Updated future waits for the pause/resume test command
* Added session connected event for when session multiplexing is disabled
* Added wait_until() to state machine to wait for callback; updated role change test

* RCORE-1973 Add role/permissions tests for new bootstrap feature (#7675)
* Moved role change tests to separate test file
* Fixed building of new flx_role_change.cpp file
* Added local changes w/role bootstrap test - fixed exception in subscription store during server initiated boostrap
* Updated local change test to include valid offline writes during role change
* Added role c... (continued)

102634 of 181458 branches covered (56.56%)

920 of 998 new or added lines in 12 files covered. (92.18%)

54 existing lines in 11 files now uncovered.

216311 of 237715 relevant lines covered (91.0%)

5880250.34 hits per line

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

95.78
/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✔
NEW
102
        REQUIRE(app_session.admin_api.patch_app_settings(
×
NEW
103
            app_session.server_app_id, {{"sync", {{"download_loop_sleep_millis", *params.sleep_millis}}}}));
×
NEW
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✔
NEW
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) {
418✔
230
            state_machina.transition_with([&](TestState cur_state) -> std::optional<TestState> {
418✔
231
                if (cur_state == TestState::not_ready || cur_state == TestState::complete)
418✔
232
                    return std::nullopt;
235✔
233

234
                using BatchState = sync::DownloadBatchState;
183✔
235
                using Event = SyncClientHookEvent;
183✔
236
                switch (data.event) {
183✔
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

NEW
246
                    case Event::SessionConnected:
✔
247
                        // Handle the reconnect if session multiplexing is disabled
NEW
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: {
25✔
266
                        // Skip unexpected download messages
267
                        if (cur_state != TestState::ident_message && cur_state != TestState::downloading) {
25✔
NEW
268
                            return std::nullopt;
×
NEW
269
                        }
×
270
                        ++download_msg_count;
25✔
271
                        // A multi-message bootstrap is in progress..
272
                        if (data.batch_state == BatchState::MoreToCome) {
25✔
273
                            // More than 1 bootstrap message, always a multi-message
274
                            bootstrap_mode = BootstrapMode::MultiMessage;
13✔
275
                            logger->trace("ROLE CHANGE: detected multi-message bootstrap");
13✔
276
                            return TestState::downloading;
13✔
277
                        }
13✔
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✔
NEW
292
                        return std::nullopt;
×
293
                    }
25✔
294

295
                    // A bootstrap message was processed
296
                    case Event::BootstrapMessageProcessed: {
25✔
297
                        REQUIRE(data.batch_state != BatchState::SteadyState);
25!
298
                        REQUIRE((cur_state == TestState::downloading || cur_state == TestState::downloaded));
25!
299
                        ++bootstrap_msg_count;
25✔
300
                        if (data.query_version == query_version) {
25✔
301
                            role_change_bootstrap = true;
25✔
302
                        }
25✔
303
                        return std::nullopt;
25✔
304
                    }
25✔
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:
85✔
311
                        return std::nullopt;
85✔
312
                }
183✔
313
            });
183✔
314
            return SyncClientHookAction::NoAction;
418✔
315
        };
418✔
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
NEW
321
            FAIL("Client reset is not expected when the role/rules/permissions are changed");
×
NEW
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✔
NEW
366
                FAIL("Failed to wait for realm update during role change bootstrap");
×
NEW
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
NEW
377
                    REQUIRE(cur_state == TestState::reconnect_received);
×
NEW
378
                    REQUIRE_FALSE(role_change_bootstrap);
×
NEW
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) {
87✔
496
                using Event = SyncClientHookEvent;
87✔
497
                if (!test_started.load()) {
87✔
498
                    return SyncClientHookAction::NoAction; // Not checking yet
63✔
499
                }
63✔
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✔
NEW
504
                    FAIL("Bootstrap occurred on the second realm, which was not expected");
×
NEW
505
                }
×
506
                return SyncClientHookAction::NoAction;
24✔
507
            };
87✔
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) {
1,090✔
688
            bootstrap_state.transition_with([&](BootstrapTestState cur_state) -> std::optional<BootstrapTestState> {
1,090✔
689
                using BatchState = sync::DownloadBatchState;
1,090✔
690
                using Event = SyncClientHookEvent;
1,090✔
691
                // Keep track of the number of bootstraps that have occurred, regardless of cur state
692
                if (data.event == Event::BootstrapProcessed) {
1,090✔
693
                    bootstrap_count++;
50✔
694
                }
50✔
695

696
                // Has the test started?
697
                if (cur_state == BootstrapTestState::not_ready)
1,090✔
698
                    return std::nullopt;
235✔
699

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

702
                switch (data.event) {
855✔
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:
168✔
718
                        bootstrap_msg_count++;
168✔
719
                        if (data.batch_state == BatchState::LastInBatch) {
168✔
720
                            new_state = BootstrapTestState::downloaded;
34✔
721
                        }
34✔
722
                        else if (data.batch_state == BatchState::MoreToCome) {
134✔
723
                            new_state = BootstrapTestState::downloading;
134✔
724
                        }
134✔
725
                        break;
168✔
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:
559✔
743
                        break;
559✔
744
                }
855✔
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 &&
855✔
748
                    update_role_state != BootstrapTestState::not_ready && bootstrap_msg_count >= update_msg_count) {
855✔
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;
855✔
754
            });
855✔
755
            return SyncClientHookAction::NoAction;
1,090✔
756
        };
1,090✔
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
NEW
762
            FAIL("Client reset is not expected when the role/rules/permissions are changed");
×
NEW
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✔
NEW
870
                    FAIL("Failed to wait for realm update during role change bootstrap");
×
NEW
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,973✔
958
                bool is_fresh_path;
1,973✔
959
                auto session = weak_session.lock();
1,973✔
960
                REQUIRE(session); // Should always be valid
1,973!
961
                is_fresh_path = _impl::client_reset::is_fresh_path(session->path());
1,973✔
962

963
                client_reset_state.transition_with(
1,973✔
964
                    [&](ClientResetTestState cur_state) -> std::optional<ClientResetTestState> {
1,973✔
965
                        using BatchState = sync::DownloadBatchState;
1,973✔
966
                        using Event = SyncClientHookEvent;
1,973✔
967

968
                        // Exit early if the test/state tracking hasn't started
969
                        if (cur_state == ClientResetTestState::not_ready)
1,973✔
970
                            return std::nullopt;
678✔
971

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

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

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

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

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

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

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

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

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

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

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

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