diff --git a/CHANGELOG.md b/CHANGELOG.md index 97caa04c161..34697a9573d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Enhancements * (PR [#????](https://github.com/realm/realm-core/pull/????)) * Performance has been improved for range queries on integers and timestamps. Requires that you use the "BETWEEN" operation in MQL or the Query::between() method when you build the query. (PR [#7785](https://github.com/realm/realm-core/pull/7785)) -* None. +* Add support for server initiated bootstraps. ([PR #7440](https://github.com/realm/realm-core/pull/7440)) ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) @@ -19,7 +19,7 @@ ----------- ### Internals -* None. +* Protocol version has been updated to v14 to support server intiated bootstraps and role change updates without a client reset. ([PR #7440](https://github.com/realm/realm-core/pull/7440)) ---------------------------------------------- diff --git a/dependencies.yml b/dependencies.yml index 86afa30f89d..cbc8ec5b841 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -3,5 +3,6 @@ VERSION: 14.10.0 OPENSSL_VERSION: 3.2.0 ZLIB_VERSION: 1.2.13 # https://github.com/10gen/baas/commits -# 9d1b4d6 is 2024 May 8 -BAAS_VERSION: 9d1b4d628babadfb606ebcadb93b1e5cae3c9565 +# 30c10fd is 2024 June 6 +BAAS_VERSION: 30c10fd8e9400fc77e594340422d8b75c210e18d +BAAS_VERSION_TYPE: githash diff --git a/evergreen/config.yml b/evergreen/config.yml index 672a1df5f04..17bddfdbbed 100644 --- a/evergreen/config.yml +++ b/evergreen/config.yml @@ -234,8 +234,9 @@ functions: shell: bash env: BAASAAS_API_KEY: "${baasaas_api_key}" + # BAAS_VERSION and VERSION_TYPE are set by realm-core/dependencies.yml BAASAAS_REF_SPEC: "${BAAS_VERSION}" - BAASAAS_START_MODE: "githash" + BAASAAS_START_MODE: "${BAAS_VERSION_TYPE|githash}" script: |- set -o errexit set -o verbose diff --git a/src/realm/object-store/sync/sync_session.hpp b/src/realm/object-store/sync/sync_session.hpp index e7b58b7f0af..d6f3ac31018 100644 --- a/src/realm/object-store/sync/sync_session.hpp +++ b/src/realm/object-store/sync/sync_session.hpp @@ -30,9 +30,9 @@ #include #include +#include #include #include -#include namespace realm { class DB; @@ -345,6 +345,8 @@ class SyncSession : public std::enable_shared_from_this { return session.get_appservices_connection_id(); } + // Supported commands can be found in `handleTestCommandMessage()` + // in baas/devicesync/server/qbs_client_handler_functions.go static util::Future send_test_command(SyncSession& session, std::string request) { return session.send_test_command(std::move(request)); diff --git a/src/realm/sync/client.cpp b/src/realm/sync/client.cpp index b67e4803a44..f2c36d3c7c0 100644 --- a/src/realm/sync/client.cpp +++ b/src/realm/sync/client.cpp @@ -803,12 +803,8 @@ void SessionImpl::update_subscription_version_info() bool SessionImpl::process_flx_bootstrap_message(const SyncProgress& progress, DownloadBatchState batch_state, int64_t query_version, const ReceivedChangesets& received_changesets) { - // Ignore the call if the session is not active - if (m_state != State::Active) { - return false; - } - - if (is_steady_state_download_message(batch_state, query_version)) { + // Ignore the message if the session is not active or a steady state message + if (m_state != State::Active || batch_state == DownloadBatchState::SteadyState) { return false; } @@ -904,7 +900,7 @@ void SessionImpl::process_pending_flx_bootstrap() auto pending_batch = bootstrap_store->peek_pending(m_wrapper.m_flx_bootstrap_batch_size_bytes); if (!pending_batch.progress) { logger.info("Incomplete pending bootstrap found for query version %1", pending_batch.query_version); - // Close the write transation before clearing the bootstrap store to avoid a deadlock because the + // Close the write transaction before clearing the bootstrap store to avoid a deadlock because the // bootstrap store requires a write transaction itself. transact->close(); bootstrap_store->clear(); @@ -1047,7 +1043,7 @@ SyncClientHookAction SessionImpl::call_debug_hook(SyncClientHookEvent event, con return call_debug_hook(data); } -SyncClientHookAction SessionImpl::call_debug_hook(SyncClientHookEvent event, const ProtocolErrorInfo& error_info) +SyncClientHookAction SessionImpl::call_debug_hook(SyncClientHookEvent event, const ProtocolErrorInfo* error_info) { if (REALM_LIKELY(!m_wrapper.m_debug_hook)) { return SyncClientHookAction::NoAction; @@ -1061,37 +1057,12 @@ SyncClientHookAction SessionImpl::call_debug_hook(SyncClientHookEvent event, con data.batch_state = DownloadBatchState::SteadyState; data.progress = m_progress; data.num_changesets = 0; - data.query_version = 0; - data.error_info = &error_info; + data.query_version = m_last_sent_flx_query_version; + data.error_info = error_info; return call_debug_hook(data); } -SyncClientHookAction SessionImpl::call_debug_hook(SyncClientHookEvent event) -{ - return call_debug_hook(event, m_progress, m_last_sent_flx_query_version, DownloadBatchState::SteadyState, 0); -} - -bool SessionImpl::is_steady_state_download_message(DownloadBatchState batch_state, int64_t query_version) -{ - // Should never be called if session is not active - REALM_ASSERT_EX(m_state == State::Active, m_state); - if (batch_state == DownloadBatchState::SteadyState) { - return true; - } - - if (!m_is_flx_sync_session) { - return true; - } - - // If this is a steady state DOWNLOAD, no need for special handling. - if (batch_state == DownloadBatchState::LastInBatch && query_version == m_wrapper.m_flx_active_version) { - return true; - } - - return false; -} - void SessionImpl::init_progress_handler() { if (m_state != State::Unactivated && m_state != State::Active) diff --git a/src/realm/sync/config.hpp b/src/realm/sync/config.hpp index adf659d60dd..1c9b2122508 100644 --- a/src/realm/sync/config.hpp +++ b/src/realm/sync/config.hpp @@ -131,7 +131,10 @@ enum class SyncClientHookEvent { ErrorMessageReceived, SessionActivating, SessionSuspended, + SessionConnected, + SessionResumed, BindMessageSent, + IdentMessageSent, ClientResetMergeComplete, BootstrapBatchAboutToProcess, }; diff --git a/src/realm/sync/noinst/client_impl_base.cpp b/src/realm/sync/noinst/client_impl_base.cpp index c898c5b1b30..b8bbc8ed4fb 100644 --- a/src/realm/sync/noinst/client_impl_base.cpp +++ b/src/realm/sync/noinst/client_impl_base.cpp @@ -1824,12 +1824,7 @@ void Session::send_message() if (!m_bind_message_sent) return send_bind_message(); // Throws - if (!m_ident_message_sent) { - if (have_client_file_ident()) - send_ident_message(); // Throws - return; - } - + // Pending test commands can be sent any time after the BIND message is sent const auto has_pending_test_command = std::any_of(m_pending_test_commands.begin(), m_pending_test_commands.end(), [](const PendingTestCommand& command) { return command.pending; @@ -1838,6 +1833,12 @@ void Session::send_message() return send_test_command_message(); } + if (!m_ident_message_sent) { + if (have_client_file_ident()) + send_ident_message(); // Throws + return; + } + if (m_error_to_send) return send_json_error_message(); // Throws @@ -1908,7 +1909,6 @@ void Session::send_bind_message() bool need_client_file_ident = !have_client_file_ident(); const bool is_subserver = false; - ClientProtocol& protocol = m_conn.get_client_protocol(); int protocol_version = m_conn.get_negotiated_protocol_version(); OutputBuffer& out = m_conn.get_output_buffer(); @@ -1992,6 +1992,7 @@ void Session::send_ident_message() m_conn.initiate_write_message(out, this); // Throws m_ident_message_sent = true; + call_debug_hook(SyncClientHookEvent::IdentMessageSent); // Other messages may be waiting to be sent enlist_to_send(); // Throws @@ -2392,22 +2393,17 @@ Status Session::receive_download_message(const DownloadMessage& message) if (!is_flx || query_version > 0) enable_progress_notifications(); - // If this is a PBS connection, then every download message is its own complete batch. - bool last_in_batch = is_flx ? *message.last_in_batch : true; - auto batch_state = last_in_batch ? sync::DownloadBatchState::LastInBatch : sync::DownloadBatchState::MoreToCome; - if (is_steady_state_download_message(batch_state, query_version)) - batch_state = DownloadBatchState::SteadyState; - auto&& progress = message.progress; if (is_flx) { logger.debug("Received: DOWNLOAD(download_server_version=%1, download_client_version=%2, " "latest_server_version=%3, latest_server_version_salt=%4, " "upload_client_version=%5, upload_server_version=%6, progress_estimate=%7, " - "last_in_batch=%8, query_version=%9, num_changesets=%10, ...)", + "batch_state=%8, query_version=%9, num_changesets=%10, ...)", progress.download.server_version, progress.download.last_integrated_client_version, progress.latest_server_version.version, progress.latest_server_version.salt, progress.upload.client_version, progress.upload.last_integrated_server_version, - message.progress_estimate, last_in_batch, query_version, message.changesets.size()); // Throws + message.progress_estimate, message.batch_state, query_version, + message.changesets.size()); // Throws } else { logger.debug("Received: DOWNLOAD(download_server_version=%1, download_client_version=%2, " @@ -2451,6 +2447,7 @@ Status Session::receive_download_message(const DownloadMessage& message) changeset.remote_version, server_version, progress.download.server_version)}; } server_version = changeset.remote_version; + // Check that per-changeset last integrated client version is "weakly" // increasing. bool good_client_version = @@ -2476,7 +2473,7 @@ Status Session::receive_download_message(const DownloadMessage& message) } auto hook_action = call_debug_hook(SyncClientHookEvent::DownloadMessageReceived, progress, query_version, - batch_state, message.changesets.size()); + message.batch_state, message.changesets.size()); if (hook_action == SyncClientHookAction::EarlyReturn) { return Status::OK(); } @@ -2485,16 +2482,16 @@ Status Session::receive_download_message(const DownloadMessage& message) if (is_flx) update_download_estimate(message.progress_estimate); - if (process_flx_bootstrap_message(progress, batch_state, query_version, message.changesets)) { + if (process_flx_bootstrap_message(progress, message.batch_state, query_version, message.changesets)) { clear_resumption_delay_state(); return Status::OK(); } uint64_t downloadable_bytes = is_flx ? 0 : message.downloadable_bytes; - initiate_integrate_changesets(downloadable_bytes, batch_state, progress, message.changesets); // Throws + initiate_integrate_changesets(downloadable_bytes, message.batch_state, progress, message.changesets); // Throws hook_action = call_debug_hook(SyncClientHookEvent::DownloadMessageIntegrated, progress, query_version, - batch_state, message.changesets.size()); + message.batch_state, message.changesets.size()); if (hook_action == SyncClientHookAction::EarlyReturn) { return Status::OK(); } @@ -2604,7 +2601,7 @@ Status Session::receive_error_message(const ProtocolErrorInfo& info) // Can't process debug hook actions once the Session is undergoing deactivation, since // the SessionWrapper may not be available if (m_state == Active) { - auto debug_action = call_debug_hook(SyncClientHookEvent::ErrorMessageReceived, info); + auto debug_action = call_debug_hook(SyncClientHookEvent::ErrorMessageReceived, &info); if (debug_action == SyncClientHookAction::EarlyReturn) { return Status::OK(); } @@ -2664,7 +2661,7 @@ void Session::suspend(const SessionErrorInfo& info) // Notify the application of the suspension of the session if the session is // still in the Active state if (m_state == Active) { - call_debug_hook(SyncClientHookEvent::SessionSuspended, info); + call_debug_hook(SyncClientHookEvent::SessionSuspended, &info); m_conn.one_less_active_unsuspended_session(); // Throws on_suspended(info); // Throws } diff --git a/src/realm/sync/noinst/client_impl_base.hpp b/src/realm/sync/noinst/client_impl_base.hpp index 8efd17c4084..81cf47b8771 100644 --- a/src/realm/sync/noinst/client_impl_base.hpp +++ b/src/realm/sync/noinst/client_impl_base.hpp @@ -827,7 +827,7 @@ class ClientImpl::Session { /// To be used in connection with implementations of /// initiate_integrate_changesets(). void integrate_changesets(const SyncProgress&, std::uint_fast64_t downloadable_bytes, const ReceivedChangesets&, - VersionInfo&, DownloadBatchState last_in_batch); + VersionInfo&, DownloadBatchState batch_state); /// To be used in connection with implementations of /// initiate_integrate_changesets(). @@ -1179,11 +1179,8 @@ class ClientImpl::Session { SyncClientHookAction call_debug_hook(SyncClientHookEvent event, const SyncProgress&, int64_t, DownloadBatchState, size_t); - SyncClientHookAction call_debug_hook(SyncClientHookEvent event, const ProtocolErrorInfo&); + SyncClientHookAction call_debug_hook(SyncClientHookEvent event, const ProtocolErrorInfo* = nullptr); SyncClientHookAction call_debug_hook(const SyncClientHookData& data); - SyncClientHookAction call_debug_hook(SyncClientHookEvent event); - - bool is_steady_state_download_message(DownloadBatchState batch_state, int64_t query_version); void init_progress_handler(); void enable_progress_notifications(); @@ -1470,6 +1467,10 @@ inline void ClientImpl::Session::connection_established(bool fast_reconnect) ++m_target_download_mark; } + // Notify the debug hook of the SessionConnected event before sending + // the bind messsage + call_debug_hook(SyncClientHookEvent::SessionConnected); + if (!m_suspended) { // Ready to send BIND message enlist_to_send(); // Throws @@ -1540,6 +1541,10 @@ inline void ClientImpl::Session::initiate_rebind() reset_protocol_state(); + // Notify the debug hook of the SessionResumed event before sending + // the bind messsage + call_debug_hook(SyncClientHookEvent::SessionResumed); + // Ready to send BIND message enlist_to_send(); // Throws } diff --git a/src/realm/sync/noinst/pending_bootstrap_store.cpp b/src/realm/sync/noinst/pending_bootstrap_store.cpp index 680e1e6e28d..90322c76154 100644 --- a/src/realm/sync/noinst/pending_bootstrap_store.cpp +++ b/src/realm/sync/noinst/pending_bootstrap_store.cpp @@ -175,6 +175,7 @@ void PendingBootstrapStore::add_batch(int64_t query_version, util::Optionalcommit(); @@ -183,15 +184,18 @@ void PendingBootstrapStore::add_batch(int64_t query_version, util::Optional query_version; - std::optional last_in_batch; + std::optional query_version; // FLX sync only + sync::DownloadBatchState batch_state = sync::DownloadBatchState::SteadyState; union { uint64_t downloadable_bytes = 0; double progress_estimate; @@ -439,10 +439,15 @@ class ClientProtocol { if (is_flx) { message.query_version = msg.read_next(); if (message.query_version < 0) - return report_error(ErrorCodes::SyncProtocolInvariantFailed, "Bad query version", + return report_error(ErrorCodes::SyncProtocolInvariantFailed, "Bad query version: %1", message.query_version); - - message.last_in_batch = msg.read_next(); + int batch_state = msg.read_next(); + if (batch_state != static_cast(sync::DownloadBatchState::MoreToCome) && + batch_state != static_cast(sync::DownloadBatchState::LastInBatch) && + batch_state != static_cast(sync::DownloadBatchState::SteadyState)) { + return report_error(ErrorCodes::SyncProtocolInvariantFailed, "Bad batch state: %1", batch_state); + } + message.batch_state = static_cast(batch_state); message.progress_estimate = msg.read_next(); if (message.progress_estimate < 0 || message.progress_estimate > 1) diff --git a/src/realm/sync/protocol.hpp b/src/realm/sync/protocol.hpp index 0cb2ec58582..cba383b1075 100644 --- a/src/realm/sync/protocol.hpp +++ b/src/realm/sync/protocol.hpp @@ -60,14 +60,17 @@ namespace sync { // 13 Support for syncing collections (lists and dictionaries) in Mixed columns and // collections of Mixed // +// 14 Support for server initiated bootstraps, including bootstraps for role/ +// permissions changes instead of performing a client reset when changed. +// // XX Changes: // - TBD // constexpr int get_current_protocol_version() noexcept { - // Also update the current protocol version test in flx_sync.cpp when - // updating this value - return 13; + // Also update the "flx: verify websocket protocol number and prefixes" test + // in flx_sync.cpp when updating this value + return 14; } constexpr std::string_view get_pbs_websocket_protocol_prefix() noexcept @@ -176,9 +179,9 @@ struct DownloadCursor { }; enum class DownloadBatchState { - MoreToCome, - LastInBatch, - SteadyState, + MoreToCome = 0, + LastInBatch = 1, + SteadyState = 2, }; /// Checks that `dc.last_integrated_client_version` is zero if @@ -464,6 +467,19 @@ inline std::ostream& operator<<(std::ostream& o, ProtocolErrorInfo::Action actio return o << "Invalid error action: " << int64_t(action); } +inline std::ostream& operator<<(std::ostream& o, DownloadBatchState batch_state) +{ + switch (batch_state) { + case DownloadBatchState::MoreToCome: + return o << "MoreToCome"; + case DownloadBatchState::LastInBatch: + return o << "LastInBatch"; + case DownloadBatchState::SteadyState: + return o << "SteadyState"; + } + return o << "Invalid batch state: " << int(batch_state); +} + } // namespace sync } // namespace realm diff --git a/src/realm/sync/tools/apply_to_state_command.cpp b/src/realm/sync/tools/apply_to_state_command.cpp index 4db2c47845b..68581c956fc 100644 --- a/src/realm/sync/tools/apply_to_state_command.cpp +++ b/src/realm/sync/tools/apply_to_state_command.cpp @@ -98,9 +98,7 @@ DownloadMessage DownloadMessage::parse(HeaderLineParser& msg, Logger& logger, bo ret.progress.upload.last_integrated_server_version = msg.read_next(); if (is_flx_sync) { ret.query_version = msg.read_next(); - auto last_in_batch = msg.read_next(); - ret.batch_state = - last_in_batch ? sync::DownloadBatchState::LastInBatch : sync::DownloadBatchState::MoreToCome; + ret.batch_state = static_cast(msg.read_next()); } else { ret.query_version = 0; diff --git a/test/object-store/sync/flx_sync.cpp b/test/object-store/sync/flx_sync.cpp index eb27a8958d8..659faaf4ddf 100644 --- a/test/object-store/sync/flx_sync.cpp +++ b/test/object-store/sync/flx_sync.cpp @@ -1638,18 +1638,9 @@ TEST_CASE("flx: uploading an object that is out-of-view results in compensating {"queryable_str_field", PropertyType::String | PropertyType::Nullable}, }}}; - AppCreateConfig::ServiceRole role; - role.name = "compensating_write_perms"; - - AppCreateConfig::ServiceRoleDocumentFilters doc_filters; - doc_filters.read = true; - doc_filters.write = {{"queryable_str_field", {{"$in", nlohmann::json::array({"foo", "bar"})}}}}; - role.document_filters = doc_filters; - - role.insert_filter = true; - role.delete_filter = true; - role.read = true; - role.write = true; + AppCreateConfig::ServiceRole role{"compensating_write_perms"}; + role.document_filters.write = {{"queryable_str_field", {{"$in", nlohmann::json::array({"foo", "bar"})}}}}; + FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}, {role}}; harness.emplace("flx_bad_query", server_schema); } @@ -2648,7 +2639,7 @@ TEST_CASE("flx: writes work without waiting for sync", "[sync][flx][baas]") { TEST_CASE("flx: verify websocket protocol number and prefixes", "[sync][protocol]") { // Update the expected value whenever the protocol version is updated - this ensures // that the current protocol version does not change unexpectedly. - REQUIRE(13 == sync::get_current_protocol_version()); + REQUIRE(14 == sync::get_current_protocol_version()); // This was updated in Protocol V8 to use '#' instead of '/' to support the Web SDK REQUIRE("com.mongodb.realm-sync#" == sync::get_pbs_websocket_protocol_prefix()); REQUIRE("com.mongodb.realm-query-sync#" == sync::get_flx_websocket_protocol_prefix()); @@ -3596,18 +3587,8 @@ TEST_CASE("flx: data ingest - dev mode", "[sync][flx][data ingest][baas]") { } TEST_CASE("flx: data ingest - write not allowed", "[sync][flx][data ingest][baas]") { - AppCreateConfig::ServiceRole role; - role.name = "asymmetric_write_perms"; - - AppCreateConfig::ServiceRoleDocumentFilters doc_filters; - doc_filters.read = true; - doc_filters.write = false; - role.document_filters = doc_filters; - - role.insert_filter = true; - role.delete_filter = true; - role.read = true; - role.write = true; + AppCreateConfig::ServiceRole role{"asymmetric_write_perms"}; + role.document_filters.write = false; Schema schema({ {"Asymmetric", @@ -4085,19 +4066,10 @@ TEST_CASE("flx: convert flx sync realm to bundled realm", "[app][flx][baas]") { } TEST_CASE("flx: compensating write errors get re-sent across sessions", "[sync][flx][compensating write][baas]") { - AppCreateConfig::ServiceRole role; - role.name = "compensating_write_perms"; - - AppCreateConfig::ServiceRoleDocumentFilters doc_filters; - doc_filters.read = true; - doc_filters.write = - nlohmann::json{{"queryable_str_field", nlohmann::json{{"$in", nlohmann::json::array({"foo", "bar"})}}}}; - role.document_filters = doc_filters; - - role.insert_filter = true; - role.delete_filter = true; - role.read = true; - role.write = true; + AppCreateConfig::ServiceRole role{"compensating_write_perms"}; + role.document_filters.write = { + {"queryable_str_field", nlohmann::json{{"$in", nlohmann::json::array({"foo", "bar"})}}}}; + FLXSyncTestHarness::ServerSchema server_schema{ g_simple_embedded_obj_schema, {"queryable_str_field", "queryable_int_field"}, {role}}; FLXSyncTestHarness::Config harness_config("flx_bad_query", server_schema); @@ -5049,6 +5021,464 @@ TEST_CASE("flx: nested collections in mixed", "[sync][flx][baas]") { CHECK(nested_list.get_any(1) == "foo"); } +TEST_CASE("flx: role change bootstrap", "[sync][flx][baas][role_change][bootstrap]") { + const Schema g_person_schema{{"Person", + {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, + {"role", PropertyType::String}, + {"firstName", PropertyType::String}, + {"lastName", PropertyType::String}}}}; + + auto fill_person_schema = [](SharedRealm realm, std::string role, size_t count) { + CppContext c(realm); + for (size_t i = 0; i < count; ++i) { + auto obj = Object::create(c, realm, "Person", + std::any(AnyDict{ + {"_id", ObjectId::gen()}, + {"role", role}, + {"firstName", util::format("%1-%2", role, i)}, + {"lastName", util::format("last-name-%1", i)}, + })); + } + }; + + enum BootstrapMode { NoReconnect, None, SingleMessage, SingleMessageMulti, MultiMessage, Any }; + + struct TestParams { + size_t num_emps = 500; + size_t num_mgrs = 10; + size_t num_dirs = 5; + std::optional num_objects = std::nullopt; + }; + + struct ExpectedResults { + BootstrapMode bootstrap; + size_t emps; + size_t mgrs; + size_t dirs; + }; + + enum TestState { + not_ready, + start, + reconnect_received, + session_resumed, + ident_message, + downloading, + downloaded, + complete + }; + TestingStateMachine state_machina(TestState::not_ready); + int64_t query_version = 0; + BootstrapMode bootstrap_mode = BootstrapMode::None; + size_t download_msg_count = 0; + size_t bootstrap_msg_count = 0; + bool role_change_bootstrap = false; + bool send_test_command = false; + auto logger = util::Logger::get_default_logger(); + + auto setup_harness = [&](FLXSyncTestHarness& harness, TestParams params) { + auto& app_session = harness.session().app_session(); + /** TODO: Remove once the server has been updated to use the protocol version */ + // Enable the role change bootstraps + REQUIRE( + app_session.admin_api.set_feature_flag(app_session.server_app_id, "allow_permissions_bootstrap", true)); + REQUIRE(app_session.admin_api.get_feature_flag(app_session.server_app_id, "allow_permissions_bootstrap")); + + if (params.num_objects) { + REQUIRE(app_session.admin_api.patch_app_settings( + app_session.server_app_id, + {{"sync", {{"num_objects_before_bootstrap_flush", *params.num_objects}}}})); + } + + // Initialize the realm with some data + harness.load_initial_data([&](SharedRealm realm) { + fill_person_schema(realm, "employee", params.num_emps); + fill_person_schema(realm, "manager", params.num_mgrs); + fill_person_schema(realm, "director", params.num_dirs); + }); + }; + + auto pause_download_builder = [](std::weak_ptr weak_session, bool pause) { + if (auto session = weak_session.lock()) { + nlohmann::json test_command = {{"command", pause ? "PAUSE_DOWNLOAD_BUILDER" : "RESUME_DOWNLOAD_BUILDER"}}; + SyncSession::OnlyForTesting::send_test_command(*session, test_command.dump()) + .get_async([](StatusWith result) { + REQUIRE(result.is_ok()); // Future completed successfully + REQUIRE(result.get_value() == "{}"); // Command completed successfully + }); + } + }; + + auto setup_config_callbacks = [&](SyncTestFile& config) { + // Use the sync client event hook to check for the error received and for tracking + // download messages and bootstraps + config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr weak_session, + const SyncClientHookData& data) { + state_machina.transition_with([&](TestState cur_state) -> std::optional { + if (cur_state == TestState::not_ready || cur_state == TestState::complete) + return std::nullopt; + + using BatchState = sync::DownloadBatchState; + using Event = SyncClientHookEvent; + switch (data.event) { + case Event::ErrorMessageReceived: + REQUIRE(cur_state == TestState::start); + REQUIRE(data.error_info); + REQUIRE(data.error_info->raw_error_code == 200); + REQUIRE(data.error_info->server_requests_action == + sync::ProtocolErrorInfo::Action::Transient); + REQUIRE_FALSE(data.error_info->is_fatal); + return TestState::reconnect_received; + + case Event::SessionConnected: + // Handle the reconnect if session multiplexing is disabled + [[fallthrough]]; + case Event::SessionResumed: + if (send_test_command) { + REQUIRE(cur_state == TestState::reconnect_received); + logger->trace("ROLE CHANGE: sending PAUSE test command after resumed"); + pause_download_builder(weak_session, true); + } + return TestState::session_resumed; + + case Event::IdentMessageSent: + if (send_test_command) { + REQUIRE(cur_state == TestState::session_resumed); + logger->trace("ROLE CHANGE: sending RESUME test command after ident message sent"); + pause_download_builder(weak_session, false); + } + return TestState::ident_message; + + case Event::DownloadMessageReceived: { + // Skip unexpected download messages + if (cur_state != TestState::ident_message && cur_state != TestState::downloading) { + return std::nullopt; + } + ++download_msg_count; + // A multi-message bootstrap is in progress.. + if (data.batch_state == BatchState::MoreToCome) { + // More than 1 bootstrap message, always a multi-message + bootstrap_mode = BootstrapMode::MultiMessage; + logger->trace("ROLE CHANGE: detected multi-message bootstrap"); + return TestState::downloading; + } + // single bootstrap message or last message in the multi-message bootstrap + else if (data.batch_state == BatchState::LastInBatch) { + if (download_msg_count == 1) { + if (data.num_changesets == 1) { + logger->trace("ROLE CHANGE: detected single-message/single-changeset bootstrap"); + bootstrap_mode = BootstrapMode::SingleMessage; + } + else { + logger->trace("ROLE CHANGE: detected single-message/multi-changeset bootstrap"); + bootstrap_mode = BootstrapMode::SingleMessageMulti; + } + } + return TestState::downloaded; + } + return std::nullopt; + } + + // A bootstrap message was processed + case Event::BootstrapMessageProcessed: { + REQUIRE(data.batch_state != BatchState::SteadyState); + REQUIRE((cur_state == TestState::downloading || cur_state == TestState::downloaded)); + ++bootstrap_msg_count; + if (data.query_version == query_version) { + role_change_bootstrap = true; + } + return std::nullopt; + } + // The bootstrap has been received and processed + case Event::BootstrapProcessed: + REQUIRE(cur_state == TestState::downloaded); + return TestState::complete; + + default: + return std::nullopt; + } + }); + return SyncClientHookAction::NoAction; + }; + + // Add client reset callback to verify a client reset doesn't happen + config.sync_config->notify_before_client_reset = [&](std::shared_ptr) { + // Make sure a client reset did not occur while waiting for the role change to + // be applied + FAIL("Client reset is not expected when the role/rules/permissions are changed"); + }; + }; + + auto set_up_realm = [](SharedRealm realm, size_t expected_cnt) { + // Set up the initial subscription + auto table = realm->read_group().get_table("class_Person"); + auto new_subs = realm->get_latest_subscription_set().make_mutable_copy(); + new_subs.insert_or_assign(Query(table)); + auto subs = new_subs.commit(); + + // Wait for subscription update and sync to complete + subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get(); + REQUIRE(!wait_for_download(*realm)); + REQUIRE(!wait_for_upload(*realm)); + wait_for_advance(*realm); + + // Verify the data was downloaded + table = realm->read_group().get_table("class_Person"); + Results results(realm, Query(table)); + REQUIRE(results.size() == expected_cnt); + }; + + auto verify_records = [](SharedRealm check_realm, ExpectedResults expected) { + // Validate the expected number of entries for each role type after the role change + auto table = check_realm->read_group().get_table("class_Person"); + REQUIRE(table->size() == (expected.emps + expected.mgrs + expected.dirs)); + auto role_col = table->get_column_key("role"); + auto table_query = Query(table).equal(role_col, "employee"); + auto results = Results(check_realm, table_query); + CHECK(results.size() == expected.emps); + table_query = Query(table).equal(role_col, "manager"); + results = Results(check_realm, table_query); + CHECK(results.size() == expected.mgrs); + table_query = Query(table).equal(role_col, "director"); + results = Results(check_realm, table_query); + CHECK(results.size() == expected.dirs); + }; + + auto update_role = [](nlohmann::json& rule, nlohmann::json doc_filter) { + rule["roles"][0]["document_filters"]["read"] = doc_filter; + rule["roles"][0]["document_filters"]["write"] = doc_filter; + }; + + auto update_perms_and_verify = [&](FLXSyncTestHarness& harness, SharedRealm check_realm, nlohmann::json new_rules, + ExpectedResults expected) { + // Reset the state machine + state_machina.transition_with([&](TestState cur_state) { + REQUIRE(cur_state == TestState::not_ready); + bootstrap_msg_count = 0; + download_msg_count = 0; + role_change_bootstrap = false; + query_version = check_realm->get_active_subscription_set().version(); + if (expected.bootstrap == BootstrapMode::SingleMessageMulti) { + send_test_command = true; + } + return TestState::start; + }); + + // Update the permissions on the server - should send an error to the client to force + // it to reconnect + auto& app_session = harness.session().app_session(); + logger->debug("Updating rule definitions: %1", new_rules); + app_session.admin_api.update_default_rule(app_session.server_app_id, new_rules); + + if (expected.bootstrap != BootstrapMode::NoReconnect) { + // After updating the permissions (if they are different), the server should send an + // error that will disconnect/reconnect the session - verify the reconnect occurs. + // Make sure at least the reconnect state (or later) has been reached + auto state_reached = state_machina.wait_until([](TestState cur_state) { + return static_cast(cur_state) >= static_cast(TestState::reconnect_received); + }); + REQUIRE(state_reached); + } + + // Assuming the session disconnects and reconnects, the server initiated role change + // bootstrap download will take place when the session is re-established and will + // complete before the server sends the initial MARK response. + REQUIRE(!wait_for_download(*check_realm)); + REQUIRE(!wait_for_upload(*check_realm)); + + // Now that the server initiated bootstrap should be complete, verify the operation + // performed matched what was expected. + state_machina.transition_with([&](TestState cur_state) { + switch (expected.bootstrap) { + case BootstrapMode::NoReconnect: + // Confirm that the session did receive an error and a bootstrap did not occur + REQUIRE(cur_state == TestState::start); + REQUIRE_FALSE(role_change_bootstrap); + break; + case BootstrapMode::None: + // Confirm that a bootstrap nor a client reset did not occur + REQUIRE(cur_state == TestState::reconnect_received); + REQUIRE_FALSE(role_change_bootstrap); + break; + case BootstrapMode::Any: + // Doesn't matter which one, just that a bootstrap occurred and not a client reset + REQUIRE(cur_state == TestState::complete); + REQUIRE(role_change_bootstrap); + break; + default: + // By the time the MARK response is received and wait_for_download() + // returns, the bootstrap should have already been applied. + REQUIRE(expected.bootstrap == bootstrap_mode); + REQUIRE(role_change_bootstrap); + REQUIRE(cur_state == TestState::complete); + if (expected.bootstrap == BootstrapMode::SingleMessageMulti || + expected.bootstrap == BootstrapMode::SingleMessage) { + REQUIRE(bootstrap_msg_count == 1); + } + else if (expected.bootstrap == BootstrapMode::MultiMessage) { + REQUIRE(bootstrap_msg_count > 1); + } + break; + } + return std::nullopt; // Don't transition + }); + + // Validate the expected number of entries for each role type after the role change + wait_for_advance(*check_realm); + verify_records(check_realm, expected); + + // Reset the state machine to "not ready" before leaving + state_machina.transition_to(TestState::not_ready); + }; + + auto setup_test = [&](FLXSyncTestHarness& harness, TestParams params, nlohmann::json initial_rules, + size_t initial_count) { + // Set up the test harness and data with the provided initial parameters + setup_harness(harness, params); + + // If an intial set of rules are provided, then set them now + if (!initial_rules.empty()) { + logger->trace("ROLE CHANGE: Initial rule definitions: %1", initial_rules); + auto& app_session = harness.session().app_session(); + app_session.admin_api.update_default_rule(app_session.server_app_id, initial_rules); + } + + // Create and set up a new realm to be returned; wait for data sync + auto config = harness.make_test_file(); + setup_config_callbacks(config); + auto realm = Realm::get_shared_realm(config); + set_up_realm(realm, initial_count); + return realm; + }; + + SECTION("Role changes lead to objects in/out of view without client reset") { + FLXSyncTestHarness harness("flx_role_change_bootstrap", {g_person_schema, {"role", "firstName", "lastName"}}); + // Get the current rules so it can be updated during the test + auto& app_session = harness.session().app_session(); + auto test_rules = app_session.admin_api.get_default_rule(app_session.server_app_id); + + // 5000 emps, 100 mgrs, 25 dirs + // num_objects_before_bootstrap_flush: 10 + TestParams params{5000, 100, 25, 10}; + auto num_total = params.num_emps + params.num_mgrs + params.num_dirs; + auto realm = setup_test(harness, params, {}, num_total); + + // Single message bootstrap - remove employees, keep mgrs/dirs + logger->trace("ROLE CHANGE: Updating rules to remove employees"); + update_role(test_rules, {{"role", {{"$in", {"manager", "director"}}}}}); + update_perms_and_verify(harness, realm, test_rules, + {BootstrapMode::SingleMessage, 0, params.num_mgrs, params.num_dirs}); + // Write the same rules again - the client should not receive the reconnect (200) error + logger->trace("ROLE CHANGE: Updating same rules again and verify reconnect doesn't happen"); + update_perms_and_verify(harness, realm, test_rules, + {BootstrapMode::NoReconnect, 0, params.num_mgrs, params.num_dirs}); + // Multi-message bootstrap - add employeees, remove managers and directors + logger->trace("ROLE CHANGE: Updating rules to add back the employees and remove mgrs/dirs"); + update_role(test_rules, {{"role", "employee"}}); + update_perms_and_verify(harness, realm, test_rules, {BootstrapMode::MultiMessage, params.num_emps, 0, 0}); + // Single message/multi-changeset bootstrap - add back the managers and directors + logger->trace("ROLE CHANGE: Updating rules to allow all records"); + update_role(test_rules, true); + update_perms_and_verify( + harness, realm, test_rules, + {BootstrapMode::SingleMessageMulti, params.num_emps, params.num_mgrs, params.num_dirs}); + } + SECTION("Role changes for one user do not change unaffected user") { + FLXSyncTestHarness harness("flx_role_change_bootstrap", {g_person_schema, {"role", "firstName", "lastName"}}); + // Get the current rules so it can be updated during the test + auto& app_session = harness.session().app_session(); + auto default_rule = app_session.admin_api.get_default_rule(app_session.server_app_id); + // 500 emps, 10 mgrs, 5 dirs + TestParams params{}; + size_t num_total = params.num_emps + params.num_mgrs + params.num_dirs; + auto realm_1 = setup_test(harness, params, {}, num_total); + + // Get the config for the first user + auto config_1 = harness.make_test_file(); + // Create a second user and a new realm config for that user + create_user_and_log_in(harness.app()); + auto config_2 = harness.make_test_file(); + REQUIRE(config_1.sync_config->user->user_id() != config_2.sync_config->user->user_id()); + + // Start with a default rule that only allows access to the employee records + AppCreateConfig::ServiceRole general_role{"default"}; + general_role.document_filters.read = {{"role", "employee"}}; + general_role.document_filters.write = {{"role", "employee"}}; + + auto rules = app_session.admin_api.get_default_rule(app_session.server_app_id); + rules["roles"][0] = {transform_service_role(general_role)}; + { + auto realm_2 = Realm::get_shared_realm(config_2); + REQUIRE(!wait_for_download(*realm_2)); + REQUIRE(!wait_for_upload(*realm_2)); + set_up_realm(realm_2, num_total); + + // Add the initial rule and verify the data in realm 1 and 2 (both should just have the employees) + update_perms_and_verify(harness, realm_1, rules, {BootstrapMode::Any, params.num_emps, 0, 0}); + REQUIRE(!wait_for_download(*realm_2)); + REQUIRE(!wait_for_upload(*realm_2)); + wait_for_advance(*realm_2); + verify_records(realm_2, {BootstrapMode::None, params.num_emps, 0, 0}); + } + { + // Reopen realm 2 and add a hook callback to check for a bootstrap (which should not happen) + std::mutex realm2_mutex; + bool realm_2_bootstrap_detected = false; + config_2.sync_config->on_sync_client_event_hook = [&](std::weak_ptr, + const SyncClientHookData& data) { + using Event = SyncClientHookEvent; + if ((data.event == Event::DownloadMessageReceived && + data.batch_state != sync::DownloadBatchState::SteadyState) || + data.event == Event::BootstrapMessageProcessed || data.event == Event::BootstrapProcessed) { + // If a download message was received or bootstrap was processed, then record it occurred + std::lock_guard lock(realm2_mutex); + realm_2_bootstrap_detected = true; + } + return SyncClientHookAction::NoAction; + }; + auto realm_2 = Realm::get_shared_realm(config_2); + REQUIRE(!wait_for_download(*realm_2)); + REQUIRE(!wait_for_upload(*realm_2)); + verify_records(realm_2, {BootstrapMode::None, params.num_emps, 0, 0}); + { + // Reset the realm_2 state for the next rule change + std::lock_guard lock(realm2_mutex); + realm_2_bootstrap_detected = false; + } + // The first rule allows access to all records for user 1 + AppCreateConfig::ServiceRole user1_role{"user 1 role"}; + user1_role.apply_when = {{"%%user.id", config_1.sync_config->user->user_id()}}; + // Add two rules, the first applies to user 1 and the second applies to other users + rules["roles"] = {transform_service_role(user1_role), transform_service_role(general_role)}; + update_perms_and_verify(harness, realm_1, rules, + {BootstrapMode::Any, params.num_emps, params.num_mgrs, params.num_dirs}); + + // Realm 2 data should not change (and there shouldn't be any bootstrap messages) + { + std::lock_guard lock(realm2_mutex); + REQUIRE_FALSE(realm_2_bootstrap_detected); + } + verify_records(realm_2, {BootstrapMode::None, params.num_emps, 0, 0}); + + // The first rule will be updated to only have access to employee and managers + AppCreateConfig::ServiceRole user1_role_2 = user1_role; + user1_role_2.document_filters.read = {{"role", {{"$in", {"employee", "manager"}}}}}; + user1_role_2.document_filters.write = {{"role", {{"$in", {"employee", "manager"}}}}}; + // Update the first rule for user 1 and verify the data after the rule is applied + rules["roles"][0] = {transform_service_role(user1_role_2)}; + update_perms_and_verify(harness, realm_1, rules, + {BootstrapMode::Any, params.num_emps, params.num_mgrs, 0}); + + // Realm 2 data should not change (and there shouldn't be any bootstrap messages) + { + std::lock_guard lock(realm2_mutex); + REQUIRE_FALSE(realm_2_bootstrap_detected); + } + verify_records(realm_2, {BootstrapMode::None, params.num_emps, 0, 0}); + } + } +} + } // namespace realm::app #endif // REALM_ENABLE_AUTH_TESTS diff --git a/test/object-store/util/sync/baas_admin_api.cpp b/test/object-store/util/sync/baas_admin_api.cpp index 4b0f5546e8a..a93d46e1983 100644 --- a/test/object-store/util/sync/baas_admin_api.cpp +++ b/test/object-store/util/sync/baas_admin_api.cpp @@ -715,8 +715,8 @@ app::Response AdminAPIEndpoint::del() const nlohmann::json AdminAPIEndpoint::get_json(const std::vector>& params) const { auto resp = get(params); - REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, - util::format("url: %1, reply: %2", m_url, resp.body)); + REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, resp.http_status_code, + resp.body); return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body); } @@ -732,7 +732,8 @@ app::Response AdminAPIEndpoint::post(std::string body) const nlohmann::json AdminAPIEndpoint::post_json(nlohmann::json body) const { auto resp = post(body.dump()); - REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(), resp.body); + REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(), + resp.http_status_code, resp.body); return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body); } @@ -748,8 +749,8 @@ app::Response AdminAPIEndpoint::put(std::string body) const nlohmann::json AdminAPIEndpoint::put_json(nlohmann::json body) const { auto resp = put(body.dump()); - REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, - util::format("url: %1 request: %2, reply: %3", m_url, body.dump(), resp.body)); + REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(), + resp.http_status_code, resp.body); return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body); } @@ -765,8 +766,8 @@ app::Response AdminAPIEndpoint::patch(std::string body) const nlohmann::json AdminAPIEndpoint::patch_json(nlohmann::json body) const { auto resp = patch(body.dump()); - REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, - util::format("url: %1 request: %2, reply: %3", m_url, body.dump(), resp.body)); + REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(), + resp.http_status_code, resp.body); return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body); } @@ -985,6 +986,60 @@ void AdminAPISession::create_schema(const std::string& app_id, const AppCreateCo } } +bool AdminAPISession::set_feature_flag(const std::string& app_id, const std::string& flag_name, bool enable) const +{ + auto features = apps(APIFamily::Private)[app_id]["features"]; + auto flag_response = + features.post_json(nlohmann::json{{"action", enable ? "enable" : "disable"}, {"feature_flags", {flag_name}}}); + return flag_response.empty(); +} + +bool AdminAPISession::get_feature_flag(const std::string& app_id, const std::string& flag_name) const +{ + auto features = apps(APIFamily::Private)[app_id]["features"]; + auto response = features.get_json(); + if (auto feature_list = response["enabled"]; !feature_list.empty()) { + return std::find_if(feature_list.begin(), feature_list.end(), [&flag_name](const auto& feature) { + return feature == flag_name; + }) != feature_list.end(); + } + return false; +} + +nlohmann::json AdminAPISession::get_default_rule(const std::string& app_id) const +{ + auto baas_sync_service = get_sync_service(app_id); + auto rule_endpoint = apps()[app_id]["services"][baas_sync_service.id]["default_rule"]; + auto rule = rule_endpoint.get_json(); + return rule; +} + +bool AdminAPISession::update_default_rule(const std::string& app_id, nlohmann::json rule_json) const +{ + if (auto id = rule_json.find("_id"); + id == rule_json.end() || !id->is_string() || id->get().empty()) { + return false; + } + + auto baas_sync_service = get_sync_service(app_id); + auto rule_endpoint = apps()[app_id]["services"][baas_sync_service.id]["default_rule"]; + auto response = rule_endpoint.put_json(rule_json); + return response.empty(); +} + +nlohmann::json AdminAPISession::get_app_settings(const std::string& app_id) const +{ + auto settings_endpoint = apps(APIFamily::Private)[app_id]["settings"]; + return settings_endpoint.get_json(); +} + +bool AdminAPISession::patch_app_settings(const std::string& app_id, nlohmann::json&& json) const +{ + auto settings_endpoint = apps(APIFamily::Private)[app_id]["settings"]; + auto response = settings_endpoint.patch_json(std::move(json)); + return response.empty(); +} + static nlohmann::json convert_config(AdminAPISession::ServiceConfig config) { if (config.mode == AdminAPISession::ServiceConfig::SyncMode::Flexible) { @@ -1371,6 +1426,23 @@ AppCreateConfig minimal_app_config(const std::string& name, const Schema& schema }; } +nlohmann::json transform_service_role(const AppCreateConfig::ServiceRole& role_def) +{ + return { + {"name", role_def.name}, + {"apply_when", role_def.apply_when}, + {"document_filters", + { + {"read", role_def.document_filters.read}, + {"write", role_def.document_filters.write}, + }}, + {"insert", role_def.insert_filter}, + {"delete", role_def.delete_filter}, + {"read", role_def.read}, + {"write", role_def.write}, + }; +} + AppSession create_app(const AppCreateConfig& config) { auto session = AdminAPISession::login(config); @@ -1509,36 +1581,11 @@ AppSession create_app(const AppCreateConfig& config) auto default_rule = services[mongo_service_id]["default_rule"]; auto service_roles = nlohmann::json::array(); if (config.service_roles.empty()) { - service_roles = nlohmann::json::array({{{"name", "default"}, - {"apply_when", nlohmann::json::object()}, - {"document_filters", - { - {"read", true}, - {"write", true}, - }}, - {"read", true}, - {"write", true}, - {"insert", true}, - {"delete", true}}}); + service_roles.push_back(transform_service_role({"default"})); } else { std::transform(config.service_roles.begin(), config.service_roles.end(), std::back_inserter(service_roles), - [](const AppCreateConfig::ServiceRole& role_def) { - nlohmann::json ret{ - {"name", role_def.name}, - {"apply_when", role_def.apply_when}, - {"document_filters", - { - {"read", role_def.document_filters.read}, - {"write", role_def.document_filters.write}, - }}, - {"insert", role_def.insert_filter}, - {"delete", role_def.delete_filter}, - {"read", role_def.read}, - {"write", role_def.write}, - }; - return ret; - }); + transform_service_role); } default_rule.post_json({{"roles", service_roles}}); @@ -1609,11 +1656,12 @@ AppSession create_app(const AppCreateConfig& config) return object_schema.table_type == ObjectSchema::ObjectType::TopLevel; }); if (any_sync_types) { + // Increasing timeout due to occasional slow startup of the translator on baasaas timed_sleeping_wait_for( [&] { return session.is_initial_sync_complete(app_id); }, - std::chrono::seconds(30), std::chrono::seconds(1)); + std::chrono::seconds(60), std::chrono::seconds(1)); } return {client_app_id, app_id, session, config}; diff --git a/test/object-store/util/sync/baas_admin_api.hpp b/test/object-store/util/sync/baas_admin_api.hpp index 46253a60f7e..78d67861593 100644 --- a/test/object-store/util/sync/baas_admin_api.hpp +++ b/test/object-store/util/sync/baas_admin_api.hpp @@ -83,6 +83,10 @@ class AdminAPISession { void trigger_client_reset(const std::string& app_id, int64_t file_ident) const; void migrate_to_flx(const std::string& app_id, const std::string& service_id, bool migrate_to_flx) const; void create_schema(const std::string& app_id, const AppCreateConfig& config, bool use_draft = true) const; + bool set_feature_flag(const std::string& app_id, const std::string& flag_name, bool enable) const; + bool get_feature_flag(const std::string& app_id, const std::string& flag_name) const; + nlohmann::json get_default_rule(const std::string& app_id) const; + bool update_default_rule(const std::string& app_id, nlohmann::json roles) const; struct Service { std::string id; @@ -140,6 +144,8 @@ class AdminAPISession { }; MigrationStatus get_migration_status(const std::string& app_id) const; + nlohmann::json get_app_settings(const std::string& app_id) const; + bool patch_app_settings(const std::string& app_id, nlohmann::json&& new_settings) const; const std::string& admin_url() const noexcept { @@ -199,17 +205,17 @@ struct AppCreateConfig { // document_filters describe which objects can be read from/written to, as // specified by the below read and write expressions. Set both to true to give read/write // access on all objects - ServiceRoleDocumentFilters document_filters; + ServiceRoleDocumentFilters document_filters = {true, true}; // insert_filter and delete_filter describe which objects can be created and erased by the client, // respectively. Set both to true if all objects can be created/erased by the client - nlohmann::json insert_filter; - nlohmann::json delete_filter; + nlohmann::json insert_filter = true; + nlohmann::json delete_filter = true; // read and write describe the permissions for "read-all-fields"/"write-all-fields" behavior. Set both to true // if all fields should have read/write access - nlohmann::json read; - nlohmann::json write; + nlohmann::json read = true; + nlohmann::json write = true; // NB: for more granular field-level permissions, the "fields" and "additional_fields" keys can be included in // a service role to describe which fields individually can be read/written. These fields have been omitted @@ -250,6 +256,7 @@ struct AppCreateConfig { realm::Schema get_default_schema(); AppCreateConfig default_app_config(); AppCreateConfig minimal_app_config(const std::string& name, const Schema& schema); +nlohmann::json transform_service_role(const AppCreateConfig::ServiceRole& role_def); struct AppSession { std::string client_app_id; diff --git a/test/object-store/util/sync/flx_sync_harness.hpp b/test/object-store/util/sync/flx_sync_harness.hpp index 7d08c861616..36004b14605 100644 --- a/test/object-store/util/sync/flx_sync_harness.hpp +++ b/test/object-store/util/sync/flx_sync_harness.hpp @@ -119,7 +119,7 @@ class FLXSyncTestHarness { template void load_initial_data(Func&& func) { - SyncTestFile config(m_test_session.app()->current_user(), schema(), SyncConfig::FLXSyncEnabled{}); + auto config = make_test_file(); auto realm = Realm::get_shared_realm(config); subscribe_to_all_and_bootstrap(*realm); diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index 1d1adbaddf5..4ba39aa8e4b 100644 --- a/test/object-store/util/sync/sync_test_utils.cpp +++ b/test/object-store/util/sync/sync_test_utils.cpp @@ -269,6 +269,10 @@ void wait_for_advance(Realm& realm) , target_version(*realm.latest_snapshot_version()) , done(done) { + // Are we already there... + if (realm.read_transaction_version().version >= target_version) { + done = true; + } } void did_change(std::vector const&, std::vector const&, bool) override diff --git a/test/object-store/util/test_utils.hpp b/test/object-store/util/test_utils.hpp index 517463494bd..c413daf6190 100644 --- a/test/object-store/util/test_utils.hpp +++ b/test/object-store/util/test_utils.hpp @@ -68,14 +68,26 @@ class TestingStateMachine { m_cv.notify_one(); } - bool wait_for(E target, std::chrono::milliseconds period = std::chrono::seconds(15)) + // Wait for the provided callback to return true + bool wait_until(util::UniqueFunction pred, + std::chrono::milliseconds period = std::chrono::seconds(15)) { std::unique_lock lock{m_mutex}; return m_cv.wait_for(lock, period, [&] { - return m_cur_state == target; + return pred(m_cur_state); }); } + // Wait for the state machine to reach a specific state + bool wait_for(E target, std::chrono::milliseconds period = std::chrono::seconds(15)) + { + return wait_until( + [&target](E cur_state) { + return cur_state == target; + }, + period); + } + private: std::mutex m_mutex; std::condition_variable m_cv;