From 429d17b4c447810cc7245eefd06d37cc1441b6bc Mon Sep 17 00:00:00 2001 From: James Stone Date: Mon, 18 Dec 2023 15:08:47 -0800 Subject: [PATCH] split out sync from app services tests --- .gitignore | 1 + test/object-store/CMakeLists.txt | 4 +- test/object-store/audit.cpp | 6 +- test/object-store/c_api/c_api.cpp | 2 +- test/object-store/sync/app.cpp | 5340 ----------------- .../sync/app_non_sync_services.cpp | 2803 +++++++++ test/object-store/sync/app_services.cpp | 117 - test/object-store/sync/app_sync_services.cpp | 2520 ++++++++ test/object-store/sync/client_reset.cpp | 9 +- .../connection_change_notifications.cpp | 2 - test/object-store/sync/session/session.cpp | 2 - test/object-store/sync/sync_manager.cpp | 1 - test/object-store/sync/user.cpp | 1 - .../util/sync/flx_sync_harness.hpp | 10 +- .../util/sync/sync_test_utils.cpp | 30 + .../util/sync/sync_test_utils.hpp | 6 + test/object-store/util/test_file.cpp | 87 +- test/object-store/util/test_file.hpp | 110 +- 18 files changed, 5555 insertions(+), 5496 deletions(-) delete mode 100644 test/object-store/sync/app.cpp create mode 100644 test/object-store/sync/app_non_sync_services.cpp delete mode 100644 test/object-store/sync/app_services.cpp create mode 100644 test/object-store/sync/app_sync_services.cpp diff --git a/.gitignore b/.gitignore index 8fd4a65df13..2ffc8062863 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,4 @@ tsconfig.tsbuildinfo # Baas remote host artifacts baas-work-dir/ ssh_agent_commands.sh +baas/ diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index e5ccd8d6e6a..f4ee4b856a0 100644 --- a/test/object-store/CMakeLists.txt +++ b/test/object-store/CMakeLists.txt @@ -47,6 +47,7 @@ if(REALM_ENABLE_AUTH_TESTS) util/unit_test_transport.hpp ) list(APPEND SOURCES + sync/app_non_sync_services.cpp util/sync/baas_admin_api.cpp util/unit_test_transport.cpp ) @@ -61,7 +62,7 @@ if(REALM_ENABLE_SYNC) ) list(APPEND SOURCES bson.cpp - sync/app.cpp + sync/app_sync_services.cpp sync/client_reset.cpp sync/file.cpp sync/flx_migration.cpp @@ -86,7 +87,6 @@ elseif(REALM_APP_SERVICES) util/sync/baas_admin_api.hpp ) list(APPEND SOURCES - sync/app_services.cpp util/sync/sync_test_utils.cpp ) endif() diff --git a/test/object-store/audit.cpp b/test/object-store/audit.cpp index 75acc91f641..c4e501682e3 100644 --- a/test/object-store/audit.cpp +++ b/test/object-store/audit.cpp @@ -1676,7 +1676,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { auto app_create_config = default_app_config(); app_create_config.schema = schema; app_create_config.dev_mode_enabled = false; - TestAppSession session = create_app(app_create_config); + TestAppSession session(TestAppSession::Config{create_app(app_create_config)}); SyncTestFile config(session.app()->current_user(), bson::Bson("default")); config.schema = schema; @@ -1734,7 +1734,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { // Create an app which does not include AuditEvent in the schema so that // things will break if audit tries to use it app_create_config.schema = no_audit_event_schema; - TestAppSession session_2 = create_app(app_create_config); + TestAppSession session_2(TestAppSession::Config{create_app(app_create_config)}); SyncTestFile config(session_2.app()->current_user(), bson::Bson("default")); config.schema = no_audit_event_schema; config.audit_config = std::make_shared(); @@ -1793,7 +1793,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { SECTION("AuditEvent missing from server schema") { app_create_config.schema = no_audit_event_schema; - TestAppSession session_2 = create_app(app_create_config); + TestAppSession session_2(TestAppSession::Config{create_app(app_create_config)}); SyncTestFile config(session_2.app()->current_user(), bson::Bson("default")); config.schema = no_audit_event_schema; config.audit_config = std::make_shared(); diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index ecff3a930d1..fe3f9ba423b 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -5711,7 +5711,7 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a auto user_data = new TestTransportUserData(); auto http_transport = realm_http_transport_new(send_request_to_server, user_data, user_data_free); auto app_session = get_runtime_app_session(); - TestAppSession session(app_session, *http_transport, DeleteApp{false}); + TestAppSession session({app_session, *http_transport, DeleteApp{false}}); realm_app app(session.app()); SECTION("remove_user integration") { diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp deleted file mode 100644 index 3cff8fc908e..00000000000 --- a/test/object-store/sync/app.cpp +++ /dev/null @@ -1,5340 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include "collection_fixtures.hpp" -#include "util/sync/baas_admin_api.hpp" -#include "util/sync/sync_test_utils.hpp" -#include "util/unit_test_transport.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace realm; -using namespace realm::app; -using util::any_cast; -using util::Optional; - -using namespace std::string_view_literals; -using namespace std::literals::string_literals; - -namespace { -std::shared_ptr log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) -{ - if (auto transport = dynamic_cast(app->config().transport.get())) { - transport->set_provider_type(credentials.provider_as_string()); - } - std::shared_ptr user; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user_arg, Optional error) { - REQUIRE_FALSE(error); - REQUIRE(user_arg); - user = std::move(user_arg); - }); - REQUIRE(user); - return user; -} - -AppError failed_log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) -{ - Optional err; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user, Optional error) { - REQUIRE(error); - REQUIRE_FALSE(user); - err = error; - }); - REQUIRE(err); - return *err; -} - -} // namespace - -namespace realm { -class TestHelper { -public: - static DBRef get_db(Realm& realm) - { - return Realm::Internal::get_db(realm); - } -}; -} // namespace realm - -#if REALM_ENABLE_AUTH_TESTS - -#include - -static std::string create_jwt(const std::string& appId) -{ - nlohmann::json header = {{"alg", "HS256"}, {"typ", "JWT"}}; - nlohmann::json payload = {{"aud", appId}, {"sub", "someUserId"}, {"exp", 1961896476}}; - - payload["user_data"]["name"] = "Foo Bar"; - payload["user_data"]["occupation"] = "firefighter"; - - payload["my_metadata"]["name"] = "Bar Foo"; - payload["my_metadata"]["occupation"] = "stock analyst"; - - std::string headerStr = header.dump(); - std::string payloadStr = payload.dump(); - - std::string encoded_header; - encoded_header.resize(util::base64_encoded_size(headerStr.length())); - util::base64_encode(headerStr.data(), headerStr.length(), encoded_header.data(), encoded_header.size()); - - std::string encoded_payload; - encoded_payload.resize(util::base64_encoded_size(payloadStr.length())); - util::base64_encode(payloadStr.data(), payloadStr.length(), encoded_payload.data(), encoded_payload.size()); - - // Remove padding characters. - while (encoded_header.back() == '=') - encoded_header.pop_back(); - while (encoded_payload.back() == '=') - encoded_payload.pop_back(); - - std::string jwtPayload = encoded_header + "." + encoded_payload; - - std::array hmac; - unsigned char key[] = "My_very_confidential_secretttttt"; - util::hmac_sha256(util::unsafe_span_cast(jwtPayload), hmac, util::Span(key, 32)); - - std::string signature; - signature.resize(util::base64_encoded_size(hmac.size())); - util::base64_encode(reinterpret_cast(hmac.data()), hmac.size(), signature.data(), signature.size()); - while (signature.back() == '=') - signature.pop_back(); - std::replace(signature.begin(), signature.end(), '+', '-'); - std::replace(signature.begin(), signature.end(), '/', '_'); - - return jwtPayload + "." + signature; -} - -// MARK: - Verify AppError with all error codes -TEST_CASE("app: verify app error codes", "[sync][app][local]") { - auto error_codes = ErrorCodes::get_error_list(); - std::vector> http_status_codes = { - {0, ""}, - {100, "http error code considered fatal: some http error. Informational: 100"}, - {200, ""}, - {300, "http error code considered fatal: some http error. Redirection: 300"}, - {400, "http error code considered fatal: some http error. Client Error: 400"}, - {500, "http error code considered fatal: some http error. Server Error: 500"}, - {600, "http error code considered fatal: some http error. Unknown HTTP Error: 600"}}; - - auto make_http_error = [](std::optional error_code, int http_status = 500, - std::optional error = "some error", - std::optional link = "http://dummy-link/") -> app::Response { - nlohmann::json body; - if (error_code) { - body["error_code"] = *error_code; - } - if (error) { - body["error"] = *error; - } - if (link) { - body["link"] = *link; - } - - return { - http_status, - 0, - {{"Content-Type", "application/json"}}, - body.empty() ? "{}" : body.dump(), - }; - }; - - // Success response - app::Response response = {200, 0, {}, ""}; - auto app_error = AppUtils::check_for_errors(response); - REQUIRE(!app_error); - - // Empty error code - response = make_http_error(""); - app_error = AppUtils::check_for_errors(response); - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::AppUnknownError); - REQUIRE(app_error->code_string() == "AppUnknownError"); - REQUIRE(app_error->server_error.empty()); - REQUIRE(app_error->reason() == "some error"); - REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); - REQUIRE(*app_error->additional_status_code == 500); - - // Missing error code - response = make_http_error(std::nullopt); - app_error = AppUtils::check_for_errors(response); - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::AppUnknownError); - REQUIRE(app_error->code_string() == "AppUnknownError"); - REQUIRE(app_error->server_error.empty()); - REQUIRE(app_error->reason() == "some error"); - REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); - REQUIRE(*app_error->additional_status_code == 500); - - // Missing error code and error message with success http status - response = make_http_error(std::nullopt, 200, std::nullopt); - app_error = AppUtils::check_for_errors(response); - REQUIRE(!app_error); - - for (auto [name, error] : error_codes) { - // All error codes should not cause an exception - if (error != ErrorCodes::HTTPError && error != ErrorCodes::OK) { - response = make_http_error(name); - app_error = AppUtils::check_for_errors(response); - REQUIRE(app_error); - if (ErrorCodes::error_categories(error).test(ErrorCategory::app_error)) { - REQUIRE(app_error->code() == error); - REQUIRE(app_error->code_string() == name); - } - else { - REQUIRE(app_error->code() == ErrorCodes::AppServerError); - REQUIRE(app_error->code_string() == "AppServerError"); - } - REQUIRE(app_error->server_error == name); - REQUIRE(app_error->reason() == "some error"); - REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); - REQUIRE(app_error->additional_status_code); - REQUIRE(*app_error->additional_status_code == 500); - } - } - - response = make_http_error("AppErrorMissing", 404); - app_error = AppUtils::check_for_errors(response); - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::AppServerError); - REQUIRE(app_error->code_string() == "AppServerError"); - REQUIRE(app_error->server_error == "AppErrorMissing"); - REQUIRE(app_error->reason() == "some error"); - REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); - REQUIRE(app_error->additional_status_code); - REQUIRE(*app_error->additional_status_code == 404); - - // HTTPError with different status values - for (auto [status, message] : http_status_codes) { - response = { - status, - 0, - {}, - "some http error", - }; - app_error = AppUtils::check_for_errors(response); - if (message.empty()) { - REQUIRE(!app_error); - continue; - } - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::HTTPError); - REQUIRE(app_error->code_string() == "HTTPError"); - REQUIRE(app_error->server_error.empty()); - REQUIRE(app_error->reason() == message); - REQUIRE(app_error->link_to_server_logs.empty()); - REQUIRE(app_error->additional_status_code); - REQUIRE(*app_error->additional_status_code == status); - } - - // Missing error code and error message with fatal http status - response = { - 501, - 0, - {}, - "", - }; - app_error = AppUtils::check_for_errors(response); - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::HTTPError); - REQUIRE(app_error->code_string() == "HTTPError"); - REQUIRE(app_error->server_error.empty()); - REQUIRE(app_error->reason() == "http error code considered fatal. Server Error: 501"); - REQUIRE(app_error->link_to_server_logs.empty()); - REQUIRE(app_error->additional_status_code); - REQUIRE(*app_error->additional_status_code == 501); - - // Valid client error code, with body, but no json - app::Response client_response = { - 501, - 0, - {}, - "Some error occurred", - ErrorCodes::BadBsonParse, // client_error_code - }; - app_error = AppUtils::check_for_errors(client_response); - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::BadBsonParse); - REQUIRE(app_error->code_string() == "BadBsonParse"); - REQUIRE(app_error->server_error.empty()); - REQUIRE(app_error->reason() == "Some error occurred"); - REQUIRE(app_error->link_to_server_logs.empty()); - REQUIRE(app_error->additional_status_code); - REQUIRE(*app_error->additional_status_code == 501); - - // Same response with client error code, but no body - client_response.body = ""; - app_error = AppUtils::check_for_errors(client_response); - REQUIRE(app_error); - REQUIRE(app_error->reason() == "client error code value considered fatal"); - - // Valid custom status code, with body, but no json - app::Response custom_response = {501, - 4999, // custom_status_code - {}, - "Some custom error occurred"}; - app_error = AppUtils::check_for_errors(custom_response); - REQUIRE(app_error); - REQUIRE(app_error->code() == ErrorCodes::CustomError); - REQUIRE(app_error->code_string() == "CustomError"); - REQUIRE(app_error->server_error.empty()); - REQUIRE(app_error->reason() == "Some custom error occurred"); - REQUIRE(app_error->link_to_server_logs.empty()); - REQUIRE(app_error->additional_status_code); - REQUIRE(*app_error->additional_status_code == 4999); - - // Same response with custom status code, but no body - custom_response.body = ""; - app_error = AppUtils::check_for_errors(custom_response); - REQUIRE(app_error); - REQUIRE(app_error->reason() == "non-zero custom status code considered fatal"); -} - -// MARK: - Login with Credentials Tests - -TEST_CASE("app: login_with_credentials integration", "[sync][app][user][baas]") { - SECTION("login") { - TestAppSession session; - auto app = session.app(); - app->log_out([](auto) {}); - - int subscribe_processed = 0; - auto token = app->subscribe([&subscribe_processed](auto& app) { - if (!subscribe_processed) { - REQUIRE(app.backing_store()->get_current_user()); - } - else { - REQUIRE_FALSE(app.backing_store()->get_current_user()); - } - subscribe_processed++; - }); - - auto user = log_in(app); - CHECK(!user->device_id().empty()); - CHECK(user->has_device_id()); - - bool processed = false; - app->log_out([&](auto error) { - REQUIRE_FALSE(error); - processed = true; - }); - - CHECK(processed); - CHECK(subscribe_processed == 2); - - app->unsubscribe(token); - } -} - -// MARK: - UsernamePasswordProviderClient Tests - -TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][baas]") { - const std::string base_url = get_base_url(); - AutoVerifiedEmailCredentials creds; - auto email = creds.email; - auto password = creds.password; - - TestAppSession session; - auto app = session.app(); - auto client = app->provider_client(); - - bool processed = false; - - client.register_email(email, password, [&](Optional error) { - CAPTURE(email); - CAPTURE(password); - REQUIRE_FALSE(error); // first registration success - }); - - SECTION("double registration should fail") { - client.register_email(email, password, [&](Optional error) { - // Error returned states the account has already been created - REQUIRE(error); - CHECK(error->reason() == "name already in use"); - CHECK(error->code() == ErrorCodes::AccountNameInUse); - CHECK(!error->link_to_server_logs.empty()); - CHECK(error->link_to_server_logs.find(base_url) != std::string::npos); - processed = true; - }); - CHECK(processed); - } - - SECTION("double registration should fail") { - // the server registration function will reject emails that do not contain "realm_tests_do_autoverify" - std::string email_to_reject = util::format("%1@%2.com", random_string(10), random_string(10)); - client.register_email(email_to_reject, password, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == util::format("failed to confirm user \"%1\"", email_to_reject)); - CHECK(error->code() == ErrorCodes::BadRequest); - processed = true; - }); - CHECK(processed); - } - - SECTION("can login with registered account") { - auto user = log_in(app, creds); - CHECK(user->user_profile().email() == email); - } - - SECTION("cannot login with wrong password") { - app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"), - [&](std::shared_ptr user, Optional error) { - CHECK(!user); - REQUIRE(error); - REQUIRE(error->code() == ErrorCodes::InvalidPassword); - processed = true; - }); - CHECK(processed); - } - - SECTION("confirm user") { - client.confirm_user("a_token", "a_token_id", [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "invalid token data"); - processed = true; - }); - CHECK(processed); - } - - SECTION("resend confirmation email") { - client.resend_confirmation_email(email, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "already confirmed"); - processed = true; - }); - CHECK(processed); - } - - SECTION("reset password invalid tokens") { - client.reset_password(password, "token_sample", "token_id_sample", [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "invalid token data"); - CHECK(!error->link_to_server_logs.empty()); - CHECK(error->link_to_server_logs.find(base_url) != std::string::npos); - processed = true; - }); - CHECK(processed); - } - - SECTION("reset password function success") { - // the imported test app will accept password reset if the password contains "realm_tests_do_reset" via a - // function - std::string accepted_new_password = util::format("realm_tests_do_reset%1", random_string(10)); - client.call_reset_password_function(email, accepted_new_password, {}, [&](Optional error) { - REQUIRE_FALSE(error); - processed = true; - }); - CHECK(processed); - } - - SECTION("reset password function failure") { - std::string rejected_password = util::format("%1", random_string(10)); - client.call_reset_password_function(email, rejected_password, {"foo", "bar"}, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == util::format("failed to reset password for user \"%1\"", email)); - CHECK(error->is_service_error()); - processed = true; - }); - CHECK(processed); - } - - SECTION("reset password function for invalid user fails") { - client.call_reset_password_function(util::format("%1@%2.com", random_string(5), random_string(5)), password, - {"foo", "bar"}, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "user not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::UserNotFound); - processed = true; - }); - CHECK(processed); - } - - SECTION("retry custom confirmation") { - client.retry_custom_confirmation(email, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "already confirmed"); - processed = true; - }); - CHECK(processed); - } - - SECTION("retry custom confirmation for invalid user fails") { - client.retry_custom_confirmation(util::format("%1@%2.com", random_string(5), random_string(5)), - [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "user not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::UserNotFound); - processed = true; - }); - CHECK(processed); - } - - SECTION("log in, remove, log in") { - app->remove_user(app->backing_store()->get_current_user(), [](auto) {}); - CHECK(app->backing_store()->all_users().size() == 0); - CHECK(app->backing_store()->get_current_user() == nullptr); - - auto user = log_in(app, AppCredentials::username_password(email, password)); - CHECK(user->user_profile().email() == email); - CHECK(user->state() == SyncUser::State::LoggedIn); - - app->remove_user(user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - CHECK(user->state() == SyncUser::State::Removed); - - log_in(app, AppCredentials::username_password(email, password)); - CHECK(user->state() == SyncUser::State::Removed); - CHECK(app->backing_store()->get_current_user() != user); - user = app->backing_store()->get_current_user(); - CHECK(user->user_profile().email() == email); - CHECK(user->state() == SyncUser::State::LoggedIn); - - app->remove_user(user, [&](Optional error) { - REQUIRE(!error); - CHECK(app->backing_store()->all_users().size() == 0); - processed = true; - }); - - CHECK(user->state() == SyncUser::State::Removed); - CHECK(processed); - CHECK(app->backing_store()->all_users().size() == 0); - } -} - -// MARK: - UserAPIKeyProviderClient Tests - -TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baas]") { - TestAppSession session; - auto app = session.app(); - auto client = app->provider_client(); - - bool processed = false; - App::UserAPIKey api_key; - - SECTION("api-key") { - std::shared_ptr logged_in_user = app->backing_store()->get_current_user(); - auto api_key_name = util::format("%1", random_string(15)); - client.create_api_key(api_key_name, logged_in_user, - [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.name == api_key_name); - api_key = user_api_key; - }); - - client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.name == api_key_name); - CHECK(user_api_key.id == api_key.id); - }); - - client.fetch_api_keys(logged_in_user, [&](std::vector api_keys, Optional error) { - CHECK(api_keys.size() == 1); - for (auto key : api_keys) { - CHECK(key.id.to_string() == api_key.id.to_string()); - CHECK(api_key.name == api_key_name); - CHECK(key.id == api_key.id); - } - REQUIRE_FALSE(error); - }); - - client.enable_api_key(api_key.id, logged_in_user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.disabled == false); - CHECK(user_api_key.name == api_key_name); - CHECK(user_api_key.id == api_key.id); - }); - - client.disable_api_key(api_key.id, logged_in_user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.disabled == true); - CHECK(user_api_key.name == api_key_name); - }); - - client.delete_api_key(api_key.id, logged_in_user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { - CHECK(user_api_key.name == ""); - CHECK(error); - processed = true; - }); - - CHECK(processed); - } - - SECTION("api-key without a user") { - std::shared_ptr no_user = nullptr; - auto api_key_name = util::format("%1", random_string(15)); - client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - CHECK(user_api_key.name == ""); - }); - - client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - CHECK(user_api_key.name == ""); - }); - - client.fetch_api_keys(no_user, [&](std::vector api_keys, Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - CHECK(api_keys.size() == 0); - }); - - client.enable_api_key(api_key.id, no_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - }); - - client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - CHECK(user_api_key.name == ""); - }); - - client.disable_api_key(api_key.id, no_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - }); - - client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - CHECK(user_api_key.name == ""); - }); - - client.delete_api_key(api_key.id, no_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - }); - - client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { - CHECK(user_api_key.name == ""); - REQUIRE(error); - CHECK(error->is_service_error()); - CHECK(error->reason() == "must authenticate first"); - processed = true; - }); - CHECK(processed); - } - - SECTION("api-key against the wrong user") { - std::shared_ptr first_user = app->backing_store()->get_current_user(); - create_user_and_log_in(app); - std::shared_ptr second_user = app->backing_store()->get_current_user(); - REQUIRE(first_user != second_user); - auto api_key_name = util::format("%1", random_string(15)); - App::UserAPIKey api_key; - App::UserAPIKeyProviderClient provider = app->provider_client(); - - provider.create_api_key(api_key_name, first_user, - [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.name == api_key_name); - api_key = user_api_key; - }); - - provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.name == api_key_name); - CHECK(user_api_key.id.to_string() == user_api_key.id.to_string()); - }); - - provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - CHECK(user_api_key.name == ""); - }); - - provider.fetch_api_keys(first_user, [&](std::vector api_keys, Optional error) { - CHECK(api_keys.size() == 1); - for (auto api_key : api_keys) { - CHECK(api_key.name == api_key_name); - } - REQUIRE_FALSE(error); - }); - - provider.fetch_api_keys(second_user, [&](std::vector api_keys, Optional error) { - CHECK(api_keys.size() == 0); - REQUIRE_FALSE(error); - }); - - provider.enable_api_key(api_key.id, first_user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - provider.enable_api_key(api_key.id, second_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - }); - - provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.disabled == false); - CHECK(user_api_key.name == api_key_name); - }); - - provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(user_api_key.name == ""); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - }); - - provider.disable_api_key(api_key.id, first_user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - provider.disable_api_key(api_key.id, second_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - }); - - provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.disabled == true); - CHECK(user_api_key.name == api_key_name); - }); - - provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE(error); - CHECK(user_api_key.name == ""); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - }); - - provider.delete_api_key(api_key.id, second_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - }); - - provider.delete_api_key(api_key.id, first_user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { - CHECK(user_api_key.name == ""); - REQUIRE(error); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - processed = true; - }); - - provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { - CHECK(user_api_key.name == ""); - REQUIRE(error); - CHECK(error->reason() == "API key not found"); - CHECK(error->is_service_error()); - CHECK(error->code() == ErrorCodes::APIKeyNotFound); - processed = true; - }); - - CHECK(processed); - } -} - -// MARK: - Auth Providers Function Tests - -TEST_CASE("app: auth providers function integration", "[sync][app][user][baas]") { - TestAppSession session; - auto app = session.app(); - - SECTION("auth providers function integration") { - bson::BsonDocument function_params{{"realmCustomAuthFuncUserId", "123456"}}; - auto credentials = AppCredentials::function(function_params); - auto user = log_in(app, credentials); - REQUIRE(user->identities()[0].provider_type == IdentityProviderFunction); - } -} - -// MARK: - Link User Tests - -TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { - TestAppSession session; - auto app = session.app(); - auto user = log_in(app); - - AutoVerifiedEmailCredentials creds; - app->provider_client().register_email(creds.email, creds.password, - [&](Optional error) { - REQUIRE_FALSE(error); - }); - - SECTION("anonymous users are reused before they are linked to an identity") { - REQUIRE(user == log_in(app)); - } - - SECTION("linking a user adds that identity to the user") { - REQUIRE(user->identities().size() == 1); - CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); - - app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { - REQUIRE_FALSE(error); - REQUIRE(user == user2); - REQUIRE(user->identities().size() == 2); - CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); - CHECK(user->identities()[1].provider_type == IdentityProviderUsernamePassword); - }); - } - - SECTION("linking an identity makes the user no longer returned by anonymous logins") { - app->link_user(user, creds, [&](std::shared_ptr, Optional error) { - REQUIRE_FALSE(error); - }); - auto user2 = log_in(app); - REQUIRE(user != user2); - } - - SECTION("existing users are reused when logging in via linked identities") { - app->link_user(user, creds, [](std::shared_ptr, Optional error) { - REQUIRE_FALSE(error); - }); - app->log_out([](auto error) { - REQUIRE_FALSE(error); - }); - REQUIRE(user->state() == SyncUser::State::LoggedOut); - // Should give us the same user instance despite logging in with a - // different identity - REQUIRE(user == log_in(app, creds)); - REQUIRE(user->state() == SyncUser::State::LoggedIn); - } -} - -// MARK: - Delete User Tests - -TEST_CASE("app: delete anonymous user integration", "[sync][app][user][baas]") { - TestAppSession session; - auto app = session.app(); - auto backing_store = app->backing_store(); - - SECTION("delete user expect success") { - CHECK(backing_store->all_users().size() == 1); - - // Log in user 1 - auto user_a = backing_store->get_current_user(); - CHECK(user_a->state() == SyncUser::State::LoggedIn); - app->delete_user(user_a, [&](Optional error) { - REQUIRE_FALSE(error); - // a logged out anon user will be marked as Removed, not LoggedOut - CHECK(user_a->state() == SyncUser::State::Removed); - }); - CHECK(backing_store->all_users().empty()); - CHECK(backing_store->get_current_user() == nullptr); - - app->delete_user(user_a, [&](Optional error) { - CHECK(error->reason() == "User must be logged in to be deleted."); - CHECK(backing_store->all_users().size() == 0); - }); - - // Log in user 2 - auto user_b = log_in(app); - CHECK(backing_store->get_current_user() == user_b); - CHECK(user_b->state() == SyncUser::State::LoggedIn); - CHECK(backing_store->all_users().size() == 1); - - app->delete_user(user_b, [&](Optional error) { - REQUIRE_FALSE(error); - CHECK(backing_store->all_users().size() == 0); - }); - - CHECK(backing_store->get_current_user() == nullptr); - - // check both handles are no longer valid - CHECK(user_a->state() == SyncUser::State::Removed); - CHECK(user_b->state() == SyncUser::State::Removed); - } -} - -TEST_CASE("app: delete user with credentials integration", "[sync][app][user][baas]") { - TestAppSession session; - auto app = session.app(); - auto backing_store = app->backing_store(); - app->remove_user(backing_store->get_current_user(), [](auto) {}); - - SECTION("log in and delete") { - CHECK(backing_store->all_users().size() == 0); - CHECK(backing_store->get_current_user() == nullptr); - - auto credentials = create_user_and_log_in(app); - auto user = backing_store->get_current_user(); - - CHECK(backing_store->get_current_user() == user); - CHECK(user->state() == SyncUser::State::LoggedIn); - app->delete_user(user, [&](Optional error) { - REQUIRE_FALSE(error); - CHECK(app->backing_store()->all_users().size() == 0); - }); - CHECK(user->state() == SyncUser::State::Removed); - CHECK(backing_store->get_current_user() == nullptr); - - app->log_in_with_credentials(credentials, [](std::shared_ptr user, util::Optional error) { - CHECK(!user); - REQUIRE(error); - REQUIRE(error->code() == ErrorCodes::InvalidPassword); - }); - CHECK(backing_store->get_current_user() == nullptr); - - CHECK(backing_store->all_users().size() == 0); - app->delete_user(user, [](Optional err) { - CHECK(err->code() > 0); - }); - - CHECK(backing_store->get_current_user() == nullptr); - CHECK(backing_store->all_users().size() == 0); - CHECK(user->state() == SyncUser::State::Removed); - } -} - -// MARK: - Call Function Tests - -TEST_CASE("app: call function", "[sync][app][function][baas]") { - TestAppSession session; - auto app = session.app(); - - bson::BsonArray toSum(5); - std::iota(toSum.begin(), toSum.end(), static_cast(1)); - const auto checkFn = [](Optional&& sum, Optional&& error) { - REQUIRE(!error); - CHECK(*sum == 15); - }; - app->call_function("sumFunc", toSum, checkFn); - app->call_function(app->backing_store()->get_current_user(), "sumFunc", toSum, checkFn); -} - -// MARK: - Remote Mongo Client Tests - -TEST_CASE("app: remote mongo client", "[sync][app][mongo][baas]") { - TestAppSession session; - auto app = session.app(); - - auto remote_client = app->backing_store()->get_current_user()->mongo_client("BackingDB"); - auto app_session = get_runtime_app_session(); - auto db = remote_client.db(app_session.config.mongo_dbname); - auto dog_collection = db["Dog"]; - auto cat_collection = db["Cat"]; - auto person_collection = db["Person"]; - - bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}}; - - bson::BsonDocument dog_document2{{"name", "bob"}, {"breed", "french bulldog"}}; - - auto dog3_object_id = ObjectId::gen(); - bson::BsonDocument dog_document3{ - {"_id", dog3_object_id}, - {"name", "petunia"}, - {"breed", "french bulldog"}, - }; - - auto cat_id_string = random_string(10); - bson::BsonDocument cat_document{ - {"_id", cat_id_string}, - {"name", "luna"}, - {"breed", "scottish fold"}, - }; - - bson::BsonDocument person_document{ - {"firstName", "John"}, - {"lastName", "Johnson"}, - {"age", 30}, - }; - - bson::BsonDocument person_document2{ - {"firstName", "Bob"}, - {"lastName", "Johnson"}, - {"age", 30}, - }; - - bson::BsonDocument bad_document{{"bad", "value"}}; - - dog_collection.delete_many(dog_document, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.delete_many(dog_document2, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.delete_many({}, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.delete_many(person_document, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.delete_many(person_document2, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - SECTION("insert") { - bool processed = false; - ObjectId dog_object_id; - ObjectId dog2_object_id; - - dog_collection.insert_one_bson(bad_document, [&](Optional bson, Optional error) { - CHECK(error); - CHECK(!bson); - }); - - dog_collection.insert_one_bson(dog_document3, [&](Optional value, Optional error) { - REQUIRE_FALSE(error); - auto bson = static_cast(*value); - CHECK(static_cast(bson["insertedId"]) == dog3_object_id); - }); - - cat_collection.insert_one_bson(cat_document, [&](Optional value, Optional error) { - REQUIRE_FALSE(error); - auto bson = static_cast(*value); - CHECK(static_cast(bson["insertedId"]) == cat_id_string); - }); - - dog_collection.delete_many({}, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - cat_collection.delete_one(cat_document, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.insert_one(bad_document, [&](Optional object_id, Optional error) { - CHECK(error); - CHECK(!object_id); - }); - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog_object_id = static_cast(*object_id); - }); - - dog_collection.insert_one(dog_document2, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog2_object_id = static_cast(*object_id); - }); - - dog_collection.insert_one(dog_document3, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK(object_id->type() == bson::Bson::Type::ObjectId); - CHECK(static_cast(*object_id) == dog3_object_id); - }); - - cat_collection.insert_one(cat_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK(object_id->type() == bson::Bson::Type::String); - CHECK(static_cast(*object_id) == cat_id_string); - }); - - person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id, dog3_object_id}); - person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - }); - - dog_collection.delete_many({}, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - cat_collection.delete_one(cat_document, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - bson::BsonArray documents{ - dog_document, - dog_document2, - dog_document3, - }; - - dog_collection.insert_many_bson(documents, [&](Optional value, Optional error) { - REQUIRE_FALSE(error); - auto bson = static_cast(*value); - auto insertedIds = static_cast(bson["insertedIds"]); - }); - - dog_collection.delete_many({}, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.insert_many(documents, [&](std::vector inserted_docs, Optional error) { - REQUIRE_FALSE(error); - CHECK(inserted_docs.size() == 3); - CHECK(inserted_docs[0].type() == bson::Bson::Type::ObjectId); - CHECK(inserted_docs[1].type() == bson::Bson::Type::ObjectId); - CHECK(inserted_docs[2].type() == bson::Bson::Type::ObjectId); - CHECK(static_cast(inserted_docs[2]) == dog3_object_id); - processed = true; - }); - - CHECK(processed); - } - - SECTION("find") { - bool processed = false; - - dog_collection.find(dog_document, [&](Optional document_array, Optional error) { - REQUIRE_FALSE(error); - CHECK((*document_array).size() == 0); - }); - - dog_collection.find_bson(dog_document, {}, [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK(static_cast(*bson).size() == 0); - }); - - dog_collection.find_one(dog_document, [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - CHECK(!document); - }); - - dog_collection.find_one_bson(dog_document, {}, [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK((!bson || bson::holds_alternative(*bson))); - }); - - ObjectId dog_object_id; - ObjectId dog2_object_id; - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog_object_id = static_cast(*object_id); - }); - - dog_collection.insert_one(dog_document2, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog2_object_id = static_cast(*object_id); - }); - - person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id}); - person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - }); - - dog_collection.find(dog_document, [&](Optional documents, Optional error) { - REQUIRE_FALSE(error); - CHECK((*documents).size() == 1); - }); - - dog_collection.find_bson(dog_document, {}, [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK(static_cast(*bson).size() == 1); - }); - - person_collection.find(person_document, [&](Optional documents, Optional error) { - REQUIRE_FALSE(error); - CHECK((*documents).size() == 1); - }); - - MongoCollection::FindOptions options{ - 2, // document limit - Optional({{"name", 1}, {"breed", 1}}), // project - Optional({{"breed", 1}}) // sort - }; - - dog_collection.find(dog_document, options, - [&](Optional document_array, Optional error) { - REQUIRE_FALSE(error); - CHECK((*document_array).size() == 1); - }); - - dog_collection.find({{"name", "fido"}}, options, - [&](Optional document_array, Optional error) { - REQUIRE_FALSE(error); - CHECK((*document_array).size() == 1); - auto king_charles = static_cast((*document_array)[0]); - CHECK(king_charles["breed"] == "king charles"); - }); - - dog_collection.find_one(dog_document, [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto name = (*document)["name"]; - CHECK(name == "fido"); - }); - - dog_collection.find_one(dog_document, options, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto name = (*document)["name"]; - CHECK(name == "fido"); - }); - - dog_collection.find_one_bson(dog_document, options, [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - auto name = (static_cast(*bson))["name"]; - CHECK(name == "fido"); - }); - - dog_collection.find(dog_document, [&](Optional documents, Optional error) { - REQUIRE_FALSE(error); - CHECK((*documents).size() == 1); - }); - - dog_collection.find_one_and_delete(dog_document, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - REQUIRE(document); - }); - - dog_collection.find_one_and_delete({{}}, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - REQUIRE(document); - }); - - dog_collection.find_one_and_delete({{"invalid", "key"}}, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - CHECK(!document); - }); - - dog_collection.find_one_and_delete_bson({{"invalid", "key"}}, {}, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK((!bson || bson::holds_alternative(*bson))); - }); - - dog_collection.find(dog_document, [&](Optional documents, Optional error) { - REQUIRE_FALSE(error); - CHECK((*documents).size() == 0); - processed = true; - }); - - CHECK(processed); - } - - SECTION("count and aggregate") { - bool processed = false; - - ObjectId dog_object_id; - ObjectId dog2_object_id; - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - }); - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog_object_id = static_cast(*object_id); - }); - - dog_collection.insert_one(dog_document2, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog2_object_id = static_cast(*object_id); - }); - - person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id}); - person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - }); - - bson::BsonDocument match{{"$match", bson::BsonDocument({{"name", "fido"}})}}; - - bson::BsonDocument group{{"$group", bson::BsonDocument({{"_id", "$name"}})}}; - - bson::BsonArray pipeline{match, group}; - - dog_collection.aggregate(pipeline, [&](Optional documents, Optional error) { - REQUIRE_FALSE(error); - CHECK((*documents).size() == 1); - }); - - dog_collection.aggregate_bson(pipeline, [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK(static_cast(*bson).size() == 1); - }); - - dog_collection.count({{"breed", "king charles"}}, [&](uint64_t count, Optional error) { - REQUIRE_FALSE(error); - CHECK(count == 2); - }); - - dog_collection.count_bson({{"breed", "king charles"}}, 0, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK(static_cast(*bson) == 2); - }); - - dog_collection.count({{"breed", "french bulldog"}}, [&](uint64_t count, Optional error) { - REQUIRE_FALSE(error); - CHECK(count == 1); - }); - - dog_collection.count({{"breed", "king charles"}}, 1, [&](uint64_t count, Optional error) { - REQUIRE_FALSE(error); - CHECK(count == 1); - }); - - person_collection.count( - {{"firstName", "John"}, {"lastName", "Johnson"}, {"age", bson::BsonDocument({{"$gt", 25}})}}, 1, - [&](uint64_t count, Optional error) { - REQUIRE_FALSE(error); - CHECK(count == 1); - processed = true; - }); - - CHECK(processed); - } - - SECTION("find and update") { - bool processed = false; - - MongoCollection::FindOneAndModifyOptions find_and_modify_options{ - Optional({{"name", 1}, {"breed", 1}}), // project - Optional({{"name", 1}}), // sort, - true, // upsert - true // return new doc - }; - - dog_collection.find_one_and_update(dog_document, dog_document2, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - CHECK(!document); - }); - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - }); - - dog_collection.find_one_and_update(dog_document, dog_document2, find_and_modify_options, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto breed = static_cast((*document)["breed"]); - CHECK(breed == "french bulldog"); - }); - - dog_collection.find_one_and_update(dog_document2, dog_document, find_and_modify_options, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto breed = static_cast((*document)["breed"]); - CHECK(breed == "king charles"); - }); - - dog_collection.find_one_and_update_bson(dog_document, dog_document2, find_and_modify_options, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - auto breed = static_cast( - static_cast(*bson)["breed"]); - CHECK(breed == "french bulldog"); - }); - - dog_collection.find_one_and_update_bson(dog_document2, dog_document, find_and_modify_options, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - auto breed = static_cast( - static_cast(*bson)["breed"]); - CHECK(breed == "king charles"); - }); - - dog_collection.find_one_and_update({{"name", "invalid name"}}, {{"name", "some name"}}, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - CHECK(!document); - processed = true; - }); - CHECK(processed); - processed = false; - - dog_collection.find_one_and_update({{"name", "invalid name"}}, {{}}, find_and_modify_options, - [&](Optional document, Optional error) { - REQUIRE(error); - CHECK(error->reason() == "insert not permitted"); - CHECK(!document); - processed = true; - }); - CHECK(processed); - } - - SECTION("update") { - bool processed = false; - ObjectId dog_object_id; - - dog_collection.update_one(dog_document, dog_document2, true, - [&](MongoCollection::UpdateResult result, Optional error) { - REQUIRE_FALSE(error); - CHECK((*result.upserted_id).to_string() != ""); - }); - - dog_collection.update_one(dog_document2, dog_document, - [&](MongoCollection::UpdateResult result, Optional error) { - REQUIRE_FALSE(error); - CHECK(!result.upserted_id); - }); - - cat_collection.update_one({}, cat_document, true, - [&](MongoCollection::UpdateResult result, Optional error) { - REQUIRE_FALSE(error); - CHECK(result.upserted_id->type() == bson::Bson::Type::String); - CHECK(result.upserted_id == cat_id_string); - }); - - dog_collection.delete_many({}, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - cat_collection.delete_many({}, [&](uint64_t, Optional error) { - REQUIRE_FALSE(error); - }); - - dog_collection.update_one_bson(dog_document, dog_document2, true, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - auto upserted_id = static_cast(*bson)["upsertedId"]; - - REQUIRE(upserted_id.type() == bson::Bson::Type::ObjectId); - }); - - dog_collection.update_one_bson(dog_document2, dog_document, true, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - auto document = static_cast(*bson); - auto foundUpsertedId = document.find("upsertedId") != document.end(); - REQUIRE(!foundUpsertedId); - }); - - cat_collection.update_one_bson({}, cat_document, true, - [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - auto upserted_id = static_cast(*bson)["upsertedId"]; - REQUIRE(upserted_id.type() == bson::Bson::Type::String); - REQUIRE(upserted_id == cat_id_string); - }); - - person_document["dogs"] = bson::BsonArray(); - bson::BsonDocument person_document_copy = bson::BsonDocument(person_document); - person_document_copy["dogs"] = bson::BsonArray({dog_object_id}); - person_collection.update_one(person_document, person_document, true, - [&](MongoCollection::UpdateResult, Optional error) { - REQUIRE_FALSE(error); - processed = true; - }); - - CHECK(processed); - } - - SECTION("update many") { - bool processed = false; - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - }); - - dog_collection.update_many(dog_document2, dog_document, true, - [&](MongoCollection::UpdateResult result, Optional error) { - REQUIRE_FALSE(error); - CHECK((*result.upserted_id).to_string() != ""); - }); - - dog_collection.update_many(dog_document2, dog_document, - [&](MongoCollection::UpdateResult result, Optional error) { - REQUIRE_FALSE(error); - CHECK(!result.upserted_id); - processed = true; - }); - - CHECK(processed); - } - - SECTION("find and replace") { - bool processed = false; - ObjectId dog_object_id; - ObjectId person_object_id; - - MongoCollection::FindOneAndModifyOptions find_and_modify_options{ - Optional({{"name", "fido"}}), // project - Optional({{"name", 1}}), // sort, - true, // upsert - true // return new doc - }; - - dog_collection.find_one_and_replace(dog_document, dog_document2, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - CHECK(!document); - }); - - dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - dog_object_id = static_cast(*object_id); - }); - - dog_collection.find_one_and_replace(dog_document, dog_document2, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto name = static_cast((*document)["name"]); - CHECK(name == "fido"); - }); - - dog_collection.find_one_and_replace(dog_document2, dog_document, find_and_modify_options, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto name = static_cast((*document)["name"]); - CHECK(static_cast(name) == "fido"); - }); - - person_document["dogs"] = bson::BsonArray({dog_object_id}); - person_document2["dogs"] = bson::BsonArray({dog_object_id}); - person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { - REQUIRE_FALSE(error); - CHECK((*object_id).to_string() != ""); - person_object_id = static_cast(*object_id); - }); - - MongoCollection::FindOneAndModifyOptions person_find_and_modify_options{ - Optional({{"firstName", 1}}), // project - Optional({{"firstName", 1}}), // sort, - false, // upsert - true // return new doc - }; - - person_collection.find_one_and_replace(person_document, person_document2, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto name = static_cast((*document)["firstName"]); - // Should return the old document - CHECK(name == "John"); - processed = true; - }); - - person_collection.find_one_and_replace(person_document2, person_document, person_find_and_modify_options, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - auto name = static_cast((*document)["firstName"]); - // Should return new document, Bob -> John - CHECK(name == "John"); - }); - - person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, - [&](Optional document, Optional error) { - // If a document is not found then null will be returned for the - // document and no error will be returned - REQUIRE_FALSE(error); - CHECK(!document); - }); - - person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, person_find_and_modify_options, - [&](Optional document, Optional error) { - REQUIRE_FALSE(error); - CHECK(!document); - processed = true; - }); - - CHECK(processed); - } - - SECTION("delete") { - - bool processed = false; - - bson::BsonArray documents; - documents.assign(3, dog_document); - - dog_collection.insert_many(documents, [&](std::vector inserted_docs, Optional error) { - REQUIRE_FALSE(error); - CHECK(inserted_docs.size() == 3); - }); - - MongoCollection::FindOneAndModifyOptions find_and_modify_options{ - Optional({{"name", "fido"}}), // project - Optional({{"name", 1}}), // sort, - true, // upsert - true // return new doc - }; - - dog_collection.delete_one(dog_document, [&](uint64_t deleted_count, Optional error) { - REQUIRE_FALSE(error); - CHECK(deleted_count >= 1); - }); - - dog_collection.delete_many(dog_document, [&](uint64_t deleted_count, Optional error) { - REQUIRE_FALSE(error); - CHECK(deleted_count >= 1); - processed = true; - }); - - person_collection.delete_many_bson(person_document, [&](Optional bson, Optional error) { - REQUIRE_FALSE(error); - CHECK(static_cast(static_cast(*bson)["deletedCount"]) >= 1); - processed = true; - }); - - CHECK(processed); - } -} - -// MARK: - Push Notifications Tests - -TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") { - TestAppSession session; - auto app = session.app(); - std::shared_ptr sync_user = app->backing_store()->get_current_user(); - - SECTION("register") { - bool processed; - - app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional error) { - REQUIRE_FALSE(error); - processed = true; - }); - - CHECK(processed); - } - /* - // FIXME: It seems this test fails when the two register_device calls are invoked too quickly, - // The error returned will be 'Device not found' on the second register_device call. - SECTION("register twice") { - // registering the same device twice should not result in an error - bool processed; - - app->push_notification_client("gcm").register_device("hello", - sync_user, - [&](Optional error) { - REQUIRE_FALSE(error); - }); - - app->push_notification_client("gcm").register_device("hello", - sync_user, - [&](Optional error) { - REQUIRE_FALSE(error); - processed = true; - }); - - CHECK(processed); - } - */ - SECTION("deregister") { - bool processed; - - app->push_notification_client("gcm").deregister_device(sync_user, [&](Optional error) { - REQUIRE_FALSE(error); - processed = true; - }); - CHECK(processed); - } - - SECTION("register with unavailable service") { - bool processed; - - app->push_notification_client("gcm_blah").register_device("hello", sync_user, [&](Optional error) { - REQUIRE(error); - CHECK(error->reason() == "service not found: 'gcm_blah'"); - processed = true; - }); - CHECK(processed); - } - - SECTION("register with logged out user") { - bool processed; - - app->log_out([=](Optional error) { - REQUIRE_FALSE(error); - }); - - app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional error) { - REQUIRE(error); - processed = true; - }); - - app->push_notification_client("gcm").register_device("hello", nullptr, [&](Optional error) { - REQUIRE(error); - processed = true; - }); - - CHECK(processed); - } -} - -// MARK: - Token refresh - -TEST_CASE("app: token refresh", "[sync][app][token][baas]") { - TestAppSession session; - auto app = session.app(); - std::shared_ptr sync_user = app->backing_store()->get_current_user(); - sync_user->update_access_token(ENCODE_FAKE_JWT("fake_access_token")); - - auto remote_client = app->backing_store()->get_current_user()->mongo_client("BackingDB"); - auto app_session = get_runtime_app_session(); - auto db = remote_client.db(app_session.config.mongo_dbname); - auto dog_collection = db["Dog"]; - bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}}; - - SECTION("access token should refresh") { - /* - Expected sequence of events: - - `find_one` tries to hit the server with a bad access token - - Server returns an error because of the bad token, error should be something like: - {\"error\":\"json: cannot unmarshal array into Go value of type map[string]interface - {}\",\"link\":\"http://localhost:9090/groups/5f84167e776aa0f9dc27081a/apps/5f841686776aa0f9dc270876/logs?co_id=5f844c8c776aa0f9dc273db6\"} - http_status_code = 401 - custom_status_code = 0 - - App::handle_auth_failure is then called and an attempt to refresh the access token will be peformed. - - If the token refresh was successful, the original request will retry and we should expect no error in the - callback of `find_one` - */ - dog_collection.find_one(dog_document, [&](Optional, Optional error) { - REQUIRE_FALSE(error); - }); - } -} - -// MARK: - Sync Tests - -TEST_CASE("app: mixed lists with object links", "[sync][pbs][app][links][baas]") { - const std::string valid_pk_name = "_id"; - - Schema schema{ - {"TopLevel", - { - {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, - {"mixed_array", PropertyType::Mixed | PropertyType::Array | PropertyType::Nullable}, - }}, - {"Target", - { - {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, - {"value", PropertyType::Int}, - }}, - }; - - auto server_app_config = minimal_app_config("set_new_embedded_object", schema); - auto app_session = create_app(server_app_config); - auto partition = random_string(100); - - auto obj_id = ObjectId::gen(); - auto target_id = ObjectId::gen(); - auto mixed_list_values = AnyVector{ - Mixed{int64_t(1234)}, - Mixed{}, - Mixed{target_id}, - }; - { - TestAppSession test_session(app_session, nullptr, DeleteApp{false}); - SyncTestFile config(test_session.app(), partition, schema); - auto realm = Realm::get_shared_realm(config); - - CppContext c(realm); - realm->begin_transaction(); - auto target_obj = Object::create( - c, realm, "Target", std::any(AnyDict{{valid_pk_name, target_id}, {"value", static_cast(1234)}})); - mixed_list_values.push_back(Mixed(target_obj.get_obj().get_link())); - - Object::create(c, realm, "TopLevel", - std::any(AnyDict{ - {valid_pk_name, obj_id}, - {"mixed_array", mixed_list_values}, - }), - CreatePolicy::ForceCreate); - realm->commit_transaction(); - CHECK(!wait_for_upload(*realm)); - } - - { - TestAppSession test_session(app_session); - SyncTestFile config(test_session.app(), partition, schema); - auto realm = Realm::get_shared_realm(config); - - CHECK(!wait_for_download(*realm)); - CppContext c(realm); - auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{obj_id}); - auto list = util::any_cast(obj.get_property_value(c, "mixed_array")); - for (size_t idx = 0; idx < list.size(); ++idx) { - Mixed mixed = list.get_any(idx); - if (idx == 3) { - CHECK(mixed.is_type(type_TypedLink)); - auto link = mixed.get(); - auto link_table = realm->read_group().get_table(link.get_table_key()); - CHECK(link_table->get_name() == "class_Target"); - auto link_obj = link_table->get_object(link.get_obj_key()); - CHECK(link_obj.get_primary_key() == target_id); - } - else { - CHECK(mixed == util::any_cast(mixed_list_values[idx])); - } - } - } -} - -TEST_CASE("app: roundtrip values", "[sync][pbs][app][baas]") { - const std::string valid_pk_name = "_id"; - - Schema schema{ - {"TopLevel", - { - {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, - {"decimal", PropertyType::Decimal | PropertyType::Nullable}, - }}, - }; - - auto server_app_config = minimal_app_config("roundtrip_values", schema); - auto app_session = create_app(server_app_config); - auto partition = random_string(100); - - Decimal128 large_significand = Decimal128(70) / Decimal128(1.09); - auto obj_id = ObjectId::gen(); - { - TestAppSession test_session(app_session, nullptr, DeleteApp{false}); - SyncTestFile config(test_session.app(), partition, schema); - auto realm = Realm::get_shared_realm(config); - - CppContext c(realm); - realm->begin_transaction(); - Object::create(c, realm, "TopLevel", - util::Any(AnyDict{ - {valid_pk_name, obj_id}, - {"decimal", large_significand}, - }), - CreatePolicy::ForceCreate); - realm->commit_transaction(); - CHECK(!wait_for_upload(*realm, std::chrono::seconds(600))); - } - - { - TestAppSession test_session(app_session); - SyncTestFile config(test_session.app(), partition, schema); - auto realm = Realm::get_shared_realm(config); - - CHECK(!wait_for_download(*realm)); - CppContext c(realm); - auto obj = Object::get_for_primary_key(c, realm, "TopLevel", util::Any{obj_id}); - auto val = obj.get_column_value("decimal"); - CHECK(val == large_significand); - } -} - -TEST_CASE("app: upgrade from local to synced realm", "[sync][pbs][app][upgrade][baas]") { - const std::string valid_pk_name = "_id"; - - Schema schema{ - {"origin", - {{valid_pk_name, PropertyType::Int, Property::IsPrimary{true}}, - {"link", PropertyType::Object | PropertyType::Nullable, "target"}, - {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"}}}, - {"target", - {{valid_pk_name, PropertyType::String, Property::IsPrimary{true}}, - {"value", PropertyType::Int}, - {"name", PropertyType::String}}}, - {"other_origin", - {{valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, - {"array", PropertyType::Array | PropertyType::Object, "other_target"}}}, - {"other_target", - {{valid_pk_name, PropertyType::UUID, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}, - {"embedded", ObjectSchema::ObjectType::Embedded, {{"name", PropertyType::String | PropertyType::Nullable}}}, - }; - - /* Create local realm */ - TestFile local_config; - local_config.schema = schema; - auto local_realm = Realm::get_shared_realm(local_config); - { - auto origin = local_realm->read_group().get_table("class_origin"); - auto target = local_realm->read_group().get_table("class_target"); - auto other_origin = local_realm->read_group().get_table("class_other_origin"); - auto other_target = local_realm->read_group().get_table("class_other_target"); - - local_realm->begin_transaction(); - auto o = target->create_object_with_primary_key("Foo").set("name", "Egon"); - // 'embedded_link' property is null. - origin->create_object_with_primary_key(47).set("link", o.get_key()); - // 'embedded_link' property is not null. - auto obj = origin->create_object_with_primary_key(42); - auto col_key = origin->get_column_key("embedded_link"); - obj.create_and_set_linked_object(col_key); - other_target->create_object_with_primary_key(UUID("3b241101-e2bb-4255-8caf-4136c566a961")); - other_origin->create_object_with_primary_key(ObjectId::gen()); - local_realm->commit_transaction(); - } - - /* Create a synced realm and upload some data */ - auto server_app_config = minimal_app_config("upgrade_from_local", schema); - TestAppSession test_session(create_app(server_app_config)); - auto partition = random_string(100); - auto user1 = test_session.app()->backing_store()->get_current_user(); - SyncTestFile config1(user1, partition, schema); - - auto r1 = Realm::get_shared_realm(config1); - - auto origin = r1->read_group().get_table("class_origin"); - auto target = r1->read_group().get_table("class_target"); - auto other_origin = r1->read_group().get_table("class_other_origin"); - auto other_target = r1->read_group().get_table("class_other_target"); - - r1->begin_transaction(); - auto o = target->create_object_with_primary_key("Baa").set("name", "Børge"); - origin->create_object_with_primary_key(47).set("link", o.get_key()); - other_target->create_object_with_primary_key(UUID("01234567-89ab-cdef-edcb-a98765432101")); - other_origin->create_object_with_primary_key(ObjectId::gen()); - r1->commit_transaction(); - CHECK(!wait_for_upload(*r1)); - - /* Copy local realm data over in a synced one*/ - create_user_and_log_in(test_session.app()); - auto user2 = test_session.app()->backing_store()->get_current_user(); - REQUIRE(user1 != user2); - - SyncTestFile config2(user1, partition, schema); - - SharedRealm r2; - SECTION("Copy before connecting to server") { - local_realm->convert(config2); - r2 = Realm::get_shared_realm(config2); - } - - SECTION("Open synced realm first") { - r2 = Realm::get_shared_realm(config2); - CHECK(!wait_for_download(*r2)); - local_realm->convert(config2); - CHECK(!wait_for_upload(*r2)); - } - - CHECK(!wait_for_download(*r2)); - advance_and_notify(*r2); - Group& g = r2->read_group(); - // g.to_json(std::cout); - REQUIRE(g.get_table("class_origin")->size() == 2); - REQUIRE(g.get_table("class_target")->size() == 2); - REQUIRE(g.get_table("class_other_origin")->size() == 2); - REQUIRE(g.get_table("class_other_target")->size() == 2); - - CHECK(!wait_for_upload(*r2)); - CHECK(!wait_for_download(*r1)); - advance_and_notify(*r1); - // r1->read_group().to_json(std::cout); -} - -TEST_CASE("app: set new embedded object", "[sync][pbs][app][baas]") { - const std::string valid_pk_name = "_id"; - - Schema schema{ - {"TopLevel", - { - {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, - {"array_of_objs", PropertyType::Object | PropertyType::Array, "TopLevel_array_of_objs"}, - {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"}, - {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable, - "TopLevel_embedded_dict"}, - }}, - {"TopLevel_array_of_objs", - ObjectSchema::ObjectType::Embedded, - { - {"array", PropertyType::Int | PropertyType::Array}, - }}, - {"TopLevel_embedded_obj", - ObjectSchema::ObjectType::Embedded, - { - {"array", PropertyType::Int | PropertyType::Array}, - }}, - {"TopLevel_embedded_dict", - ObjectSchema::ObjectType::Embedded, - { - {"array", PropertyType::Int | PropertyType::Array}, - }}, - }; - - auto server_app_config = minimal_app_config("set_new_embedded_object", schema); - TestAppSession test_session(create_app(server_app_config)); - auto partition = random_string(100); - - auto array_of_objs_id = ObjectId::gen(); - auto embedded_obj_id = ObjectId::gen(); - auto dict_obj_id = ObjectId::gen(); - - { - SyncTestFile config(test_session.app(), partition, schema); - auto realm = Realm::get_shared_realm(config); - - CppContext c(realm); - realm->begin_transaction(); - auto array_of_objs = - Object::create(c, realm, "TopLevel", - std::any(AnyDict{ - {valid_pk_name, array_of_objs_id}, - {"array_of_objs", AnyVector{AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}, - }), - CreatePolicy::ForceCreate); - - auto embedded_obj = - Object::create(c, realm, "TopLevel", - std::any(AnyDict{ - {valid_pk_name, embedded_obj_id}, - {"embedded_obj", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}, - }), - CreatePolicy::ForceCreate); - - auto dict_obj = Object::create( - c, realm, "TopLevel", - std::any(AnyDict{ - {valid_pk_name, dict_obj_id}, - {"embedded_dict", AnyDict{{"foo", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}}, - }), - CreatePolicy::ForceCreate); - - realm->commit_transaction(); - { - realm->begin_transaction(); - embedded_obj.set_property_value(c, "embedded_obj", - std::any(AnyDict{{ - "array", - AnyVector{INT64_C(3), INT64_C(4)}, - }}), - CreatePolicy::UpdateAll); - realm->commit_transaction(); - } - - { - realm->begin_transaction(); - List array(array_of_objs, array_of_objs.get_object_schema().property_for_name("array_of_objs")); - CppContext c2(realm, &array.get_object_schema()); - array.set(c2, 0, std::any{AnyDict{{"array", AnyVector{INT64_C(5), INT64_C(6)}}}}); - realm->commit_transaction(); - } - - { - realm->begin_transaction(); - object_store::Dictionary dict(dict_obj, dict_obj.get_object_schema().property_for_name("embedded_dict")); - CppContext c2(realm, &dict.get_object_schema()); - dict.insert(c2, "foo", std::any{AnyDict{{"array", AnyVector{INT64_C(7), INT64_C(8)}}}}); - realm->commit_transaction(); - } - CHECK(!wait_for_upload(*realm)); - } - - { - SyncTestFile config(test_session.app(), partition, schema); - auto realm = Realm::get_shared_realm(config); - - CHECK(!wait_for_download(*realm)); - CppContext c(realm); - { - auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{embedded_obj_id}); - auto embedded_obj = util::any_cast(obj.get_property_value(c, "embedded_obj")); - auto array_list = util::any_cast(embedded_obj.get_property_value(c, "array")); - CHECK(array_list.size() == 2); - CHECK(array_list.get(0) == int64_t(3)); - CHECK(array_list.get(1) == int64_t(4)); - } - - { - auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{array_of_objs_id}); - auto embedded_list = util::any_cast(obj.get_property_value(c, "array_of_objs")); - CppContext c2(realm, &embedded_list.get_object_schema()); - auto embedded_array_obj = util::any_cast(embedded_list.get(c2, 0)); - auto array_list = util::any_cast(embedded_array_obj.get_property_value(c2, "array")); - CHECK(array_list.size() == 2); - CHECK(array_list.get(0) == int64_t(5)); - CHECK(array_list.get(1) == int64_t(6)); - } - - { - auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{dict_obj_id}); - object_store::Dictionary dict(obj, obj.get_object_schema().property_for_name("embedded_dict")); - CppContext c2(realm, &dict.get_object_schema()); - auto embedded_obj = util::any_cast(dict.get(c2, "foo")); - auto array_list = util::any_cast(embedded_obj.get_property_value(c2, "array")); - CHECK(array_list.size() == 2); - CHECK(array_list.get(0) == int64_t(7)); - CHECK(array_list.get(1) == int64_t(8)); - } - } -} - -TEST_CASE("app: make distributable client file", "[sync][pbs][app][baas]") { - TestAppSession session; - auto app = session.app(); - - auto schema = get_default_schema(); - SyncTestFile original_config(app, bson::Bson("foo"), schema); - create_user_and_log_in(app); - SyncTestFile target_config(app, bson::Bson("foo"), schema); - - // Create realm file without client file id - { - auto realm = Realm::get_shared_realm(original_config); - - // Write some data - realm->begin_transaction(); - CppContext c; - Object::create(c, realm, "Person", - std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())}, - {"age", INT64_C(64)}, - {"firstName", std::string("Paul")}, - {"lastName", std::string("McCartney")}})); - realm->commit_transaction(); - wait_for_upload(*realm); - wait_for_download(*realm); - - realm->convert(target_config); - - // Write some additional data - realm->begin_transaction(); - Object::create(c, realm, "Dog", - std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())}, - {"breed", std::string("stabyhoun")}, - {"name", std::string("albert")}, - {"realm_id", std::string("foo")}})); - realm->commit_transaction(); - wait_for_upload(*realm); - } - // Starting a new session based on the copy - { - auto realm = Realm::get_shared_realm(target_config); - REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); - REQUIRE(realm->read_group().get_table("class_Dog")->size() == 0); - - // Should be able to download the object created in the source Realm - // after writing the copy - wait_for_download(*realm); - realm->refresh(); - REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); - REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1); - - // Check that we can continue committing to this realm - realm->begin_transaction(); - CppContext c; - Object::create(c, realm, "Dog", - std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", std::string("fido")}, - {"realm_id", std::string("foo")}})); - realm->commit_transaction(); - wait_for_upload(*realm); - } - // Original Realm should be able to read the object which was written to the copy - { - auto realm = Realm::get_shared_realm(original_config); - REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); - REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1); - - wait_for_download(*realm); - realm->refresh(); - REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); - REQUIRE(realm->read_group().get_table("class_Dog")->size() == 2); - } -} - -TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { - auto logger = util::Logger::get_default_logger(); - - const auto schema = get_default_schema(); - - auto get_dogs = [](SharedRealm r) -> Results { - wait_for_upload(*r, std::chrono::seconds(10)); - wait_for_download(*r, std::chrono::seconds(10)); - return Results(r, r->read_group().get_table("class_Dog")); - }; - - auto create_one_dog = [](SharedRealm r) { - r->begin_transaction(); - CppContext c; - Object::create(c, r, "Dog", - std::any(AnyDict{{"_id", std::any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", std::string("fido")}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); - }; - - TestAppSession session; - auto app = session.app(); - const auto partition = random_string(100); - - // MARK: Add Objects - - SECTION("Add Objects") { - { - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - - REQUIRE(get_dogs(r).size() == 0); - create_one_dog(r); - REQUIRE(get_dogs(r).size() == 1); - } - - { - create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - REQUIRE(dogs.size() == 1); - REQUIRE(dogs.get(0).get("breed") == "bulldog"); - REQUIRE(dogs.get(0).get("name") == "fido"); - } - } - - SECTION("MemOnly durability") { - { - SyncTestFile config(app, partition, schema); - config.in_memory = true; - config.encryption_key = std::vector(); - - REQUIRE(config.options().durability == DBOptions::Durability::MemOnly); - auto r = Realm::get_shared_realm(config); - - REQUIRE(get_dogs(r).size() == 0); - create_one_dog(r); - REQUIRE(get_dogs(r).size() == 1); - } - - { - create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); - config.in_memory = true; - config.encryption_key = std::vector(); - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - REQUIRE(dogs.size() == 1); - REQUIRE(dogs.get(0).get("breed") == "bulldog"); - REQUIRE(dogs.get(0).get("name") == "fido"); - } - } - - // MARK: Expired Session Refresh - - SECTION("Invalid Access Token is Refreshed") { - { - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - REQUIRE(get_dogs(r).size() == 0); - create_one_dog(r); - REQUIRE(get_dogs(r).size() == 1); - } - - { - create_user_and_log_in(app); - auto user = app->backing_store()->get_current_user(); - // set a bad access token. this will trigger a refresh when the sync session opens - user->update_access_token(encode_fake_jwt("fake_access_token")); - - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - REQUIRE(dogs.size() == 1); - REQUIRE(dogs.get(0).get("breed") == "bulldog"); - REQUIRE(dogs.get(0).get("name") == "fido"); - } - } - - class HookedTransport : public SynchronousTestTransport { - public: - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request_hook) { - request_hook(request); - } - if (simulated_response) { - return completion(*simulated_response); - } - SynchronousTestTransport::send_request_to_server(request, [&](const Response& response) mutable { - if (response_hook) { - response_hook(request, response); - } - completion(response); - }); - } - // Optional handler for the request and response before it is returned to completion - std::function response_hook; - // Optional handler for the request before it is sent to the server - std::function request_hook; - // Optional Response object to return immediately instead of communicating with the server - std::optional simulated_response; - }; - - struct HookedSocketProvider : public sync::websocket::DefaultSocketProvider { - HookedSocketProvider(const std::shared_ptr& logger, const std::string user_agent, - AutoStart auto_start = AutoStart{true}) - : DefaultSocketProvider(logger, user_agent, nullptr, auto_start) - { - } - - std::unique_ptr connect(std::unique_ptr observer, - sync::WebSocketEndpoint&& endpoint) override - { - int status_code = 101; - std::string body; - bool use_simulated_response = websocket_connect_func && websocket_connect_func(status_code, body); - - auto websocket = DefaultSocketProvider::connect(std::move(observer), std::move(endpoint)); - if (use_simulated_response) { - auto default_websocket = static_cast(websocket.get()); - if (default_websocket) - default_websocket->force_handshake_response_for_testing(status_code, body); - } - return websocket; - } - - std::function websocket_connect_func; - }; - - { - std::unique_ptr app_session; - std::string base_file_path = util::make_temp_dir() + random_string(10); - auto redir_transport = std::make_shared(); - AutoVerifiedEmailCredentials creds; - - auto app_config = get_config(redir_transport, session.app_session()); - set_app_config_defaults(app_config, redir_transport); - - util::try_make_dir(base_file_path); - SyncClientConfig sc_config; - sc_config.backing_store_config.base_file_path = base_file_path; - sc_config.backing_store_config.metadata_mode = realm::app::BackingStoreConfig::MetadataMode::NoEncryption; - - // initialize app and sync client - auto redir_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); - - SECTION("Test invalid redirect response") { - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) { - if (request_count == 0) { - logger->trace("request.url (%1): %2", request_count, request.url); - redir_transport->simulated_response = { - 301, 0, {{"Content-Type", "application/json"}}, "Some body data"}; - request_count++; - } - else if (request_count == 1) { - logger->trace("request.url (%1): %2", request_count, request.url); - redir_transport->simulated_response = { - 301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"}; - request_count++; - } - }; - - // This will fail due to no Location header - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(error); - REQUIRE(error->is_client_error()); - REQUIRE(error->code() == ErrorCodes::ClientRedirectError); - REQUIRE(error->reason() == "Redirect response missing location header"); - }); - - // This will fail due to empty Location header - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(error); - REQUIRE(error->is_client_error()); - REQUIRE(error->code() == ErrorCodes::ClientRedirectError); - REQUIRE(error->reason() == "Redirect response missing location header"); - }); - } - - SECTION("Test redirect response") { - int request_count = 0; - // redirect URL is localhost or 127.0.0.1 depending on what the initial value is - std::string original_host = "localhost:9090"; - std::string redirect_scheme = "http://"; - std::string redirect_host = "127.0.0.1:9090"; - std::string redirect_url = "http://127.0.0.1:9090"; - redir_transport->request_hook = [&](const Request& request) { - logger->trace("Received request[%1]: %2", request_count, request.url); - if (request_count == 0) { - // First request should be to location - REQUIRE(request.url.find("/location") != std::string::npos); - if (request.url.find("https://") != std::string::npos) { - redirect_scheme = "https://"; - } - // using local baas - if (request.url.find("127.0.0.1:9090") != std::string::npos) { - redirect_host = "localhost:9090"; - original_host = "127.0.0.1:9090"; - } - // using baas docker - can't test redirect - else if (request.url.find("mongodb-realm:9090") != std::string::npos) { - redirect_host = "mongodb-realm:9090"; - original_host = "mongodb-realm:9090"; - } - - redirect_url = redirect_scheme + redirect_host; - logger->trace("redirect_url (%1): %2", request_count, redirect_url); - request_count++; - } - else if (request_count == 1) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(!request.redirect_count); - redir_transport->simulated_response = { - 301, - 0, - {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, - "Some body data"}; - request_count++; - } - else if (request_count == 2) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find("somehost:9090") != std::string::npos); - redir_transport->simulated_response = { - 308, 0, {{"Location", redirect_url}, {"Content-Type", "application/json"}}, "Some body data"}; - request_count++; - } - else if (request_count == 3) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find(redirect_url) != std::string::npos); - redir_transport->simulated_response = { - 301, - 0, - {{"Location", redirect_scheme + original_host}, {"Content-Type", "application/json"}}, - "Some body data"}; - request_count++; - } - else if (request_count == 4) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos); - // Let the init_app_metadata request go through - redir_transport->simulated_response.reset(); - request_count++; - } - else if (request_count == 5) { - // This is the original request after the init app metadata - logger->trace("request.url (%1): %2", request_count, request.url); - auto backing_store = redir_app->backing_store(); - REQUIRE(backing_store); - auto app_metadata = backing_store->app_metadata(); - REQUIRE(app_metadata); - logger->trace("Deployment model: %1", app_metadata->deployment_model); - logger->trace("Location: %1", app_metadata->location); - logger->trace("Hostname: %1", app_metadata->hostname); - logger->trace("WS Hostname: %1", app_metadata->ws_hostname); - REQUIRE(app_metadata->hostname.find(original_host) != std::string::npos); - REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos); - redir_transport->simulated_response.reset(); - // Validate the retry count tracked in the original message - REQUIRE(request.redirect_count == 3); - request_count++; - } - }; - - // This will be successful after a couple of retries due to the redirect response - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(!error); - }); - } - SECTION("Test too many redirects") { - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request_count <= 21); - redir_transport->simulated_response = { - request_count % 2 == 1 ? 308 : 301, - 0, - {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, - "Some body data"}; - request_count++; - }; - - redir_app->log_in_with_credentials( - realm::app::AppCredentials::username_password(creds.email, creds.password), - [&](std::shared_ptr user, util::Optional error) { - REQUIRE(!user); - REQUIRE(error); - REQUIRE(error->is_client_error()); - REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects); - REQUIRE(error->reason() == "number of redirections exceeded 20"); - }); - } - SECTION("Test server in maintenance") { - redir_transport->request_hook = [&](const Request&) { - nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"}, - {"error", "This service is currently undergoing maintenance"}, - {"link", "https://link.to/server_logs"}}; - redir_transport->simulated_response = { - 500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()}; - }; - - redir_app->log_in_with_credentials( - realm::app::AppCredentials::username_password(creds.email, creds.password), - [&](std::shared_ptr user, util::Optional error) { - REQUIRE(!user); - REQUIRE(error); - REQUIRE(error->is_service_error()); - REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress); - REQUIRE(error->reason() == "This service is currently undergoing maintenance"); - REQUIRE(error->link_to_server_logs == "https://link.to/server_logs"); - REQUIRE(*error->additional_status_code == 500); - }); - } - } - SECTION("Test app redirect with no metadata") { - std::unique_ptr app_session; - std::string base_file_path = util::make_temp_dir() + random_string(10); - auto redir_transport = std::make_shared(); - AutoVerifiedEmailCredentials creds, creds2; - - auto app_config = get_config(redir_transport, session.app_session()); - set_app_config_defaults(app_config, redir_transport); - - util::try_make_dir(base_file_path); - SyncClientConfig sc_config; - sc_config.backing_store_config.base_file_path = base_file_path; - sc_config.backing_store_config.metadata_mode = realm::app::BackingStoreConfig::MetadataMode::NoMetadata; - - // initialize app and sync client - auto redir_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); - - int request_count = 0; - // redirect URL is localhost or 127.0.0.1 depending on what the initial value is - std::string original_host = "localhost:9090"; - std::string original_scheme = "http://"; - std::string websocket_url = "ws://some-websocket:9090"; - std::string original_url; - redir_transport->request_hook = [&](const Request& request) { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count == 0) { - // First request should be to location - REQUIRE(request.url.find("/location") != std::string::npos); - if (request.url.find("https://") != std::string::npos) { - original_scheme = "https://"; - } - // using local baas - if (request.url.find("127.0.0.1:9090") != std::string::npos) { - original_host = "127.0.0.1:9090"; - } - // using baas docker - else if (request.url.find("mongodb-realm:9090") != std::string::npos) { - original_host = "mongodb-realm:9090"; - } - original_url = original_scheme + original_host; - logger->trace("original_url (%1): %2", request_count, original_url); - } - else if (request_count == 1) { - REQUIRE(!request.redirect_count); - redir_transport->simulated_response = { - 308, - 0, - {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request_count == 2) { - REQUIRE(request.url.find("http://somehost:9090") != std::string::npos); - REQUIRE(request.url.find("location") != std::string::npos); - // app hostname will be updated via the metadata info - redir_transport->simulated_response = { - static_cast(sync::HTTPStatus::Ok), - 0, - {{"Content-Type", "application/json"}}, - util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%1\",\"ws_" - "hostname\":\"%2\"}", - original_url, websocket_url)}; - } - else { - REQUIRE(request.url.find(original_url) != std::string::npos); - redir_transport->simulated_response.reset(); - } - request_count++; - }; - - // This will be successful after a couple of retries due to the redirect response - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(!error); - }); - REQUIRE(!redir_app->backing_store()->app_metadata()); // no stored app metadata - REQUIRE(redir_app->sync_manager()->sync_route().find(websocket_url) != std::string::npos); - - // Register another email address and verify location data isn't requested again - request_count = 0; - redir_transport->request_hook = [&](const Request& request) { - logger->trace("request.url (%1): %2", request_count, request.url); - redir_transport->simulated_response.reset(); - REQUIRE(request.url.find("location") == std::string::npos); - request_count++; - }; - - redir_app->provider_client().register_email( - creds2.email, creds2.password, [&](util::Optional error) { - REQUIRE(!error); - }); - } - - SECTION("Test websocket redirect with existing session") { - std::string original_host = "localhost:9090"; - std::string redirect_scheme = "http://"; - std::string websocket_scheme = "ws://"; - std::string redirect_host = "127.0.0.1:9090"; - std::string redirect_url = "http://127.0.0.1:9090"; - - auto redir_transport = std::make_shared(); - auto redir_provider = std::make_shared(logger, ""); - std::mutex logout_mutex; - std::condition_variable logout_cv; - bool logged_out = false; - - // Use the transport to grab the current url so it can be converted - redir_transport->request_hook = [&](const Request& request) { - if (request.url.find("https://") != std::string::npos) { - redirect_scheme = "https://"; - websocket_scheme = "wss://"; - } - // using local baas - if (request.url.find("127.0.0.1:9090") != std::string::npos) { - redirect_host = "localhost:9090"; - original_host = "127.0.0.1:9090"; - } - // using baas docker - can't test redirect - else if (request.url.find("mongodb-realm:9090") != std::string::npos) { - redirect_host = "mongodb-realm:9090"; - original_host = "mongodb-realm:9090"; - } - - redirect_url = redirect_scheme + redirect_host; - logger->trace("redirect_url: %1", redirect_url); - }; - - auto server_app_config = minimal_app_config("websocket_redirect", schema); - TestAppSession test_session(create_app(server_app_config), redir_transport, DeleteApp{true}, - realm::ReconnectMode::normal, redir_provider); - auto partition = random_string(100); - auto user1 = test_session.app()->backing_store()->get_current_user(); - SyncTestFile r_config(user1, partition, schema); - // Override the default - r_config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { - if (error.status == ErrorCodes::AuthError) { - util::format(std::cerr, "Websocket redirect test: User logged out\n"); - std::unique_lock lk(logout_mutex); - logged_out = true; - logout_cv.notify_one(); - return; - } - util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n", - error.status); - abort(); - }; - - auto r = Realm::get_shared_realm(r_config); - - REQUIRE(!wait_for_download(*r)); - - SECTION("Valid websocket redirect") { - auto sync_manager = test_session.app()->sync_manager(); - auto sync_session = sync_manager->get_existing_session(r->config().path); - sync_session->pause(); - - int connect_count = 0; - redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) { - if (connect_count++ > 0) - return false; - - status_code = static_cast(sync::HTTPStatus::PermanentRedirect); - body = ""; - return true; - }; - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(original_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - REQUIRE(request.redirect_count == 0); - redir_transport->simulated_response = { - static_cast(sync::HTTPStatus::PermanentRedirect), - 0, - {{"Location", redirect_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request.url.find("/location") != std::string::npos) { - redir_transport->simulated_response = { - static_cast(sync::HTTPStatus::Ok), - 0, - {{"Content-Type", "application/json"}}, - util::format( - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_" - "hostname\":\"%3%1\"}", - redirect_host, redirect_scheme, websocket_scheme)}; - } - else { - redir_transport->simulated_response.reset(); - } - }; - - SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); - sync_session->resume(); - REQUIRE(!wait_for_download(*r)); - REQUIRE(user1->is_logged_in()); - - // Verify session is using the updated server url from the redirect - auto server_url = sync_session->full_realm_url(); - logger->trace("FULL_REALM_URL: %1", server_url); - REQUIRE((server_url && server_url->find(redirect_host) != std::string::npos)); - } - SECTION("Websocket redirect logs out user") { - auto sync_manager = test_session.app()->sync_manager(); - auto sync_session = sync_manager->get_existing_session(r->config().path); - sync_session->pause(); - - int connect_count = 0; - redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) { - if (connect_count++ > 0) - return false; - - status_code = static_cast(sync::HTTPStatus::MovedPermanently); - body = ""; - return true; - }; - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(original_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - REQUIRE(request.redirect_count == 0); - redir_transport->simulated_response = { - static_cast(sync::HTTPStatus::MovedPermanently), - 0, - {{"Location", redirect_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request.url.find("/location") != std::string::npos) { - redir_transport->simulated_response = { - static_cast(sync::HTTPStatus::Ok), - 0, - {{"Content-Type", "application/json"}}, - util::format( - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_" - "hostname\":\"%3%1\"}", - redirect_host, redirect_scheme, websocket_scheme)}; - } - else if (request.url.find("auth/session") != std::string::npos) { - redir_transport->simulated_response = {static_cast(sync::HTTPStatus::Unauthorized), - 0, - {{"Content-Type", "application/json"}}, - ""}; - } - else { - redir_transport->simulated_response.reset(); - } - }; - - SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); - sync_session->resume(); - REQUIRE(wait_for_download(*r)); - std::unique_lock lk(logout_mutex); - auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { - return logged_out; - }); - REQUIRE(result); - REQUIRE(!user1->is_logged_in()); - } - SECTION("Too many websocket redirects logs out user") { - auto sync_manager = test_session.app()->sync_manager(); - auto sync_session = sync_manager->get_existing_session(r->config().path); - sync_session->pause(); - - int connect_count = 0; - redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) { - if (connect_count++ > 0) - return false; - - status_code = static_cast(sync::HTTPStatus::MovedPermanently); - body = ""; - return true; - }; - int request_count = 0; - const int max_http_redirects = 20; // from app.cpp in object-store - redir_transport->request_hook = [&](const Request& request) { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(original_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - REQUIRE(request.redirect_count == 0); - } - if (request.url.find("/location") != std::string::npos) { - // Keep returning the redirected response - REQUIRE(request.redirect_count < max_http_redirects); - redir_transport->simulated_response = { - static_cast(sync::HTTPStatus::MovedPermanently), - 0, - {{"Location", redirect_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else { - // should not get any other types of requests during the test - the log out is local - REQUIRE(false); - } - }; - - SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); - sync_session->resume(); - REQUIRE(wait_for_download(*r)); - std::unique_lock lk(logout_mutex); - auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { - return logged_out; - }); - REQUIRE(result); - REQUIRE(!user1->is_logged_in()); - } - } - - SECTION("Fast clock on client") { - { - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - - REQUIRE(get_dogs(r).size() == 0); - create_one_dog(r); - REQUIRE(get_dogs(r).size() == 1); - } - - auto transport = std::make_shared(); - TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false}); - auto app = hooked_session.app(); - std::shared_ptr user = app->backing_store()->get_current_user(); - REQUIRE(user); - REQUIRE(!user->access_token_refresh_required()); - // Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locally - // (access tokens have an lifetime of 30 minutes today). - user->set_seconds_to_adjust_time_for_testing(31 * 60); - REQUIRE(user->access_token_refresh_required()); - - // This assumes that we make an http request for the new token while - // already in the WaitingForAccessToken state. - bool seen_waiting_for_access_token = false; - transport->request_hook = [&](const Request&) { - auto user = app->current_user(); - REQUIRE(user); - for (auto& session : app->sync_manager()->get_all_sessions_for(user)) { - // Prior to the fix for #4941, this callback would be called from an infinite loop, always in the - // WaitingForAccessToken state. - if (session->state() == SyncSession::State::WaitingForAccessToken) { - REQUIRE(!seen_waiting_for_access_token); - seen_waiting_for_access_token = true; - } - } - return true; - }; - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - REQUIRE(seen_waiting_for_access_token); - Results dogs = get_dogs(r); - REQUIRE(dogs.size() == 1); - REQUIRE(dogs.get(0).get("breed") == "bulldog"); - REQUIRE(dogs.get(0).get("name") == "fido"); - } - - SECTION("Expired Tokens") { - sync::AccessToken token; - { - std::shared_ptr user = app->backing_store()->get_current_user(); - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - - REQUIRE(get_dogs(r).size() == 0); - create_one_dog(r); - - REQUIRE(get_dogs(r).size() == 1); - sync::AccessToken::ParseError error_state = realm::sync::AccessToken::ParseError::none; - sync::AccessToken::parse(user->access_token(), token, error_state, nullptr); - REQUIRE(error_state == sync::AccessToken::ParseError::none); - REQUIRE(token.timestamp); - REQUIRE(token.expires); - REQUIRE(token.timestamp < token.expires); - std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); - using namespace std::chrono_literals; - token.expires = std::chrono::system_clock::to_time_t(now - 30s); - REQUIRE(token.expired(now)); - } - - auto transport = std::make_shared(); - TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false}); - auto app = hooked_session.app(); - std::shared_ptr user = app->backing_store()->get_current_user(); - REQUIRE(user); - REQUIRE(!user->access_token_refresh_required()); - // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. - user->update_access_token(encode_fake_jwt("fake_access_token", token.expires, token.timestamp)); - REQUIRE(user->access_token_refresh_required()); - - SECTION("Expired Access Token is Refreshed") { - // This assumes that we make an http request for the new token while - // already in the WaitingForAccessToken state. - bool seen_waiting_for_access_token = false; - transport->request_hook = [&](const Request&) { - auto user = app->current_user(); - REQUIRE(user); - for (auto& session : app->sync_manager()->get_all_sessions_for(user)) { - if (session->state() == SyncSession::State::WaitingForAccessToken) { - REQUIRE(!seen_waiting_for_access_token); - seen_waiting_for_access_token = true; - } - } - }; - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - REQUIRE(seen_waiting_for_access_token); - Results dogs = get_dogs(r); - REQUIRE(dogs.size() == 1); - REQUIRE(dogs.get(0).get("breed") == "bulldog"); - REQUIRE(dogs.get(0).get("name") == "fido"); - } - - SECTION("User is logged out if the refresh request is denied") { - REQUIRE(user->is_logged_in()); - transport->response_hook = [&](const Request& request, const Response& response) { - auto user = app->backing_store()->get_current_user(); - REQUIRE(user); - // simulate the server denying the refresh - if (request.url.find("/session") != std::string::npos) { - auto& response_ref = const_cast(response); - response_ref.http_status_code = 401; - response_ref.body = "fake: refresh token could not be refreshed"; - } - }; - SyncTestFile config(app, partition, schema); - std::atomic sync_error_handler_called{false}; - config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { - sync_error_handler_called.store(true); - REQUIRE(error.status.code() == ErrorCodes::AuthError); - REQUIRE_THAT(std::string{error.status.reason()}, - Catch::Matchers::StartsWith("Unable to refresh the user access token")); - }; - auto r = Realm::get_shared_realm(config); - timed_wait_for([&] { - return sync_error_handler_called.load(); - }); - // the failed refresh logs out the user - REQUIRE(!user->is_logged_in()); - } - - SECTION("User is left logged out if logged out while the refresh is in progress") { - REQUIRE(user->is_logged_in()); - transport->request_hook = [&](const Request&) { - user->log_out(); - }; - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - REQUIRE_FALSE(user->is_logged_in()); - REQUIRE(user->state() == SyncUser::State::LoggedOut); - } - - SECTION("Requests that receive an error are retried on a backoff") { - using namespace std::chrono; - std::vector> response_times; - std::atomic did_receive_valid_token{false}; - constexpr size_t num_error_responses = 6; - - transport->response_hook = [&](const Request& request, const Response& response) { - // simulate the server experiencing an internal server error - if (request.url.find("/session") != std::string::npos) { - if (response_times.size() >= num_error_responses) { - did_receive_valid_token.store(true); - return; - } - auto& response_ref = const_cast(response); - response_ref.http_status_code = 500; - } - }; - transport->request_hook = [&](const Request& request) { - if (!did_receive_valid_token.load() && request.url.find("/session") != std::string::npos) { - response_times.push_back(steady_clock::now()); - } - }; - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - create_one_dog(r); - timed_wait_for( - [&] { - return did_receive_valid_token.load(); - }, - 30s); - REQUIRE(user->is_logged_in()); - REQUIRE(response_times.size() >= num_error_responses); - std::vector delay_times; - for (size_t i = 1; i < response_times.size(); ++i) { - delay_times.push_back(duration_cast(response_times[i] - response_times[i - 1]).count()); - } - - // sync delays start at 1000ms minus a random number of up to 25%. - // the subsequent delay is double the previous one minus a random 25% again. - // this calculation happens in Connection::initiate_reconnect_wait() - bool increasing_delay = true; - for (size_t i = 1; i < delay_times.size(); ++i) { - if (delay_times[i - 1] >= delay_times[i]) { - increasing_delay = false; - } - } - // fail if the first delay isn't longer than half a second - if (delay_times.size() <= 1 || delay_times[1] < 500) { - increasing_delay = false; - } - if (!increasing_delay) { - std::cerr << "delay times are not increasing: "; - for (auto& delay : delay_times) { - std::cerr << delay << ", "; - } - std::cerr << std::endl; - } - REQUIRE(increasing_delay); - } - } - - SECTION("Invalid refresh token") { - auto& app_session = session.app_session(); - std::mutex mtx; - auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr user, - Realm::Config config) { - REQUIRE(user); - REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - - // requesting a new access token fails because the refresh token used for this request is revoked - user->refresh_custom_data([&](Optional error) { - REQUIRE(error); - REQUIRE(error->additional_status_code == 401); - REQUIRE(error->code() == ErrorCodes::InvalidSession); - }); - - // Set a bad access token. This will force a request for a new access token when the sync session opens - // this is only necessary because the server doesn't actually revoke previously issued access tokens - // instead allowing their session to time out as normal. So this simulates the access token expiring. - // see: - // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386 - user->update_access_token(encode_fake_jwt("fake_access_token")); - REQUIRE(!app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - - auto [sync_error_promise, sync_error] = util::make_promise_future(); - config.sync_config->error_handler = - [promise = util::CopyablePromiseHolder(std::move(sync_error_promise))](std::shared_ptr, - SyncError error) mutable { - promise.get_promise().emplace_value(std::move(error)); - }; - - auto transport = static_cast(session.transport()); - transport->block(); // don't let the token refresh happen until we're ready for it - auto r = Realm::get_shared_realm(config); - auto session = app->sync_manager()->get_existing_session(config.path); - REQUIRE(user->is_logged_in()); - REQUIRE(!sync_error.is_ready()); - { - std::atomic called{false}; - session->wait_for_upload_completion([&](Status stat) { - std::lock_guard lock(mtx); - called.store(true); - REQUIRE(stat.code() == ErrorCodes::InvalidSession); - }); - transport->unblock(); - timed_wait_for([&] { - return called.load(); - }); - std::lock_guard lock(mtx); - REQUIRE(called); - } - - auto sync_error_res = wait_for_future(std::move(sync_error)).get(); - REQUIRE(sync_error_res.status == ErrorCodes::AuthError); - REQUIRE_THAT(std::string{sync_error_res.status.reason()}, - Catch::Matchers::StartsWith("Unable to refresh the user access token")); - - // the failed refresh logs out the user - std::lock_guard lock(mtx); - REQUIRE(!user->is_logged_in()); - }; - - SECTION("Disabled user results in a sync error") { - auto creds = create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); - auto user = app->backing_store()->get_current_user(); - REQUIRE(user); - REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - app_session.admin_api.disable_user_sessions(app->current_user()->identity(), app_session.server_app_id); - - verify_error_on_sync_with_invalid_refresh_token(user, config); - - // logging in again doesn't fix things while the account is disabled - auto error = failed_log_in(app, creds); - REQUIRE(error.code() == ErrorCodes::UserDisabled); - - // admin enables user sessions again which should allow the session to continue - app_session.admin_api.enable_user_sessions(user->identity(), app_session.server_app_id); - - // logging in now works properly - log_in(app, creds); - - // still referencing the same user - REQUIRE(user == app->backing_store()->get_current_user()); - REQUIRE(user->is_logged_in()); - - { - // check that there are no errors initiating a session now by making sure upload/download succeeds - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - } - } - - SECTION("Revoked refresh token results in a sync error") { - auto creds = create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); - auto user = app->current_user(); - REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - app_session.admin_api.revoke_user_sessions(user->identity(), app_session.server_app_id); - // revoking a user session only affects the refresh token, so the access token should still continue to - // work. - REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - - verify_error_on_sync_with_invalid_refresh_token(user, config); - - // logging in again succeeds and generates a new and valid refresh token - log_in(app, creds); - - // still referencing the same user and now the user is logged in - REQUIRE(user == app->backing_store()->get_current_user()); - REQUIRE(user->is_logged_in()); - - // new requests for an access token succeed again - user->refresh_custom_data([&](Optional error) { - REQUIRE_FALSE(error); - }); - - { - // check that there are no errors initiating a new sync session by making sure upload/download - // succeeds - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - } - } - - SECTION("Revoked refresh token on an anonymous user results in a sync error") { - app->current_user()->log_out(); - auto anon_user = log_in(app); - REQUIRE(app->current_user() == anon_user); - SyncTestFile config(app, partition, schema); - REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id)); - app_session.admin_api.revoke_user_sessions(anon_user->identity(), app_session.server_app_id); - // revoking a user session only affects the refresh token, so the access token should still continue to - // work. - REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id)); - - verify_error_on_sync_with_invalid_refresh_token(anon_user, config); - - // the user has been logged out, and current user is reset - REQUIRE(!app->current_user()); - REQUIRE(!anon_user->is_logged_in()); - REQUIRE(anon_user->state() == SyncUser::State::Removed); - - // new requests for an access token do not work for anon users - anon_user->refresh_custom_data([&](Optional error) { - REQUIRE(error); - REQUIRE(error->reason() == - util::format("Cannot initiate a refresh on user '%1' because the user has been removed", - anon_user->identity())); - }); - - REQUIRE_EXCEPTION( - Realm::get_shared_realm(config), ClientUserNotFound, - util::format("Cannot start a sync session for user '%1' because this user has been removed.", - anon_user->identity())); - } - - SECTION("Opening a Realm with a removed email user results produces an exception") { - auto creds = create_user_and_log_in(app); - auto email_user = app->current_user(); - const std::string user_ident = email_user->identity(); - REQUIRE(email_user); - SyncTestFile config(app, partition, schema); - REQUIRE(email_user->is_logged_in()); - { - // sync works on a valid user - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - } - app->backing_store()->remove_user(user_ident); - REQUIRE_FALSE(email_user->is_logged_in()); - REQUIRE(email_user->state() == SyncUser::State::Removed); - - // should not be able to open a synced Realm with an invalid user - REQUIRE_EXCEPTION( - Realm::get_shared_realm(config), ClientUserNotFound, - util::format("Cannot start a sync session for user '%1' because this user has been removed.", - user_ident)); - - std::shared_ptr new_user_instance = log_in(app, creds); - // the previous instance is still invalid - REQUIRE_FALSE(email_user->is_logged_in()); - REQUIRE(email_user->state() == SyncUser::State::Removed); - // but the new instance will work and has the same server issued ident - REQUIRE(new_user_instance); - REQUIRE(new_user_instance->is_logged_in()); - REQUIRE(new_user_instance->identity() == user_ident); - { - // sync works again if the same user is logged back in - config.sync_config->user = new_user_instance; - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - } - } - } - - SECTION("large write transactions which would be too large if batched") { - SyncTestFile config(app, partition, schema); - - std::mutex mutex; - bool done = false; - auto r = Realm::get_shared_realm(config); - r->sync_session()->pause(); - - // Create 26 MB worth of dogs in 26 transactions, which should work but - // will result in an error from the server if the changesets are batched - // for upload. - CppContext c; - for (auto i = 'a'; i < 'z'; ++i) { - r->begin_transaction(); - Object::create(c, r, "Dog", - std::any(AnyDict{{"_id", std::any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", random_string(1024 * 1024)}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); - } - r->sync_session()->wait_for_upload_completion([&](Status status) { - std::lock_guard lk(mutex); - REQUIRE(status.is_ok()); - done = true; - }); - r->sync_session()->resume(); - - // If we haven't gotten an error in more than 5 minutes, then something has gone wrong - // and we should fail the test. - timed_wait_for( - [&] { - std::lock_guard lk(mutex); - return done; - }, - std::chrono::minutes(5)); - } - - SECTION("too large sync message error handling") { - SyncTestFile config(app, partition, schema); - - auto pf = util::make_promise_future(); - config.sync_config->error_handler = - [sp = util::CopyablePromiseHolder(std::move(pf.promise))](auto, SyncError error) mutable { - sp.get_promise().emplace_value(std::move(error)); - }; - auto r = Realm::get_shared_realm(config); - - // Create 26 MB worth of dogs in a single transaction - this should all get put into one changeset - // and get uploaded at once, which for now is an error on the server. - r->begin_transaction(); - CppContext c; - for (auto i = 'a'; i < 'z'; ++i) { - Object::create(c, r, "Dog", - std::any(AnyDict{{"_id", std::any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", random_string(1024 * 1024)}}), - CreatePolicy::ForceCreate); - } - r->commit_transaction(); - -#if defined(TEST_TIMEOUT_EXTRA) && TEST_TIMEOUT_EXTRA > 0 - // It may take 30 minutes to transfer 16MB at 10KB/s - auto delay = std::chrono::minutes(35); -#else - auto delay = std::chrono::minutes(5); -#endif - - auto error = wait_for_future(std::move(pf.future), delay).get(); - REQUIRE(error.status == ErrorCodes::LimitExceeded); - REQUIRE(error.status.reason() == - "Sync websocket closed because the server received a message that was too large: " - "read limited at 16777217 bytes"); - REQUIRE(error.is_client_reset_requested()); - REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset); - } - - SECTION("freezing realm does not resume session") { - SyncTestFile config(app, partition, schema); - auto realm = Realm::get_shared_realm(config); - wait_for_download(*realm); - - auto state = realm->sync_session()->state(); - REQUIRE(state == SyncSession::State::Active); - - realm->sync_session()->pause(); - state = realm->sync_session()->state(); - REQUIRE(state == SyncSession::State::Paused); - - realm->read_group(); - - { - auto frozen = realm->freeze(); - REQUIRE(realm->sync_session() == realm->sync_session()); - REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused); - } - - { - auto frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()); - REQUIRE(realm->sync_session() == realm->sync_session()); - REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused); - } - } - - SECTION("pausing a session does not hold the DB open") { - SyncTestFile config(app, partition, schema); - DBRef dbref; - std::shared_ptr sync_sess_ext_ref; - { - auto realm = Realm::get_shared_realm(config); - wait_for_download(*realm); - - auto state = realm->sync_session()->state(); - REQUIRE(state == SyncSession::State::Active); - - sync_sess_ext_ref = realm->sync_session()->external_reference(); - dbref = TestHelper::get_db(*realm); - // One ref each for the - // - RealmCoordinator - // - SyncSession - // - SessionWrapper - // - local dbref - REQUIRE(dbref.use_count() >= 4); - - realm->sync_session()->pause(); - state = realm->sync_session()->state(); - REQUIRE(state == SyncSession::State::Paused); - } - - // Closing the realm should leave one ref for the SyncSession and one for the local dbref. - REQUIRE_THAT( - [&] { - return dbref.use_count() < 4; - }, - ReturnsTrueWithinTimeLimit{}); - - // Releasing the external reference should leave one ref (the local dbref) only. - sync_sess_ext_ref.reset(); - REQUIRE_THAT( - [&] { - return dbref.use_count() == 1; - }, - ReturnsTrueWithinTimeLimit{}); - } - - SECTION("validation") { - SyncTestFile config(app, partition, schema); - - SECTION("invalid partition error handling") { - config.sync_config->partition_value = "not a bson serialized string"; - std::atomic error_did_occur = false; - config.sync_config->error_handler = [&error_did_occur](std::shared_ptr, SyncError error) { - CHECK(error.status.reason().find( - "Illegal Realm path (BIND): serialized partition 'not a bson serialized " - "string' is invalid") != std::string::npos); - error_did_occur.store(true); - }; - auto r = Realm::get_shared_realm(config); - auto session = app->sync_manager()->get_existing_session(r->config().path); - timed_wait_for([&] { - return error_did_occur.load(); - }); - REQUIRE(error_did_occur.load()); - } - - SECTION("invalid pk schema error handling") { - const std::string invalid_pk_name = "my_primary_key"; - auto it = config.schema->find("Dog"); - REQUIRE(it != config.schema->end()); - REQUIRE(it->primary_key_property()); - REQUIRE(it->primary_key_property()->name == "_id"); - it->primary_key_property()->name = invalid_pk_name; - it->primary_key = invalid_pk_name; - REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), - "The primary key property on a synchronized Realm must be named '_id' but " - "found 'my_primary_key' for type 'Dog'"); - } - - SECTION("missing pk schema error handling") { - auto it = config.schema->find("Dog"); - REQUIRE(it != config.schema->end()); - REQUIRE(it->primary_key_property()); - it->primary_key_property()->is_primary = false; - it->primary_key = ""; - REQUIRE(!it->primary_key_property()); - REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), - "There must be a primary key property named '_id' on a synchronized " - "Realm but none was found for type 'Dog'"); - } - } -} - -TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") { - TestAppSession session; - auto app = session.app(); - auto user = app->current_user(); - - SECTION("custom user data happy path") { - bool processed = false; - app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})}, - [&](auto response, auto error) { - CHECK(error == none); - CHECK(response); - CHECK(*response == true); - processed = true; - }); - CHECK(processed); - processed = false; - app->refresh_custom_data(user, [&](auto) { - processed = true; - }); - CHECK(processed); - auto data = *user->custom_data(); - CHECK(data["favorite_color"] == "green"); - } -} - -TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") { - TestAppSession session; - auto app = session.app(); - auto jwt = create_jwt(session.app()->config().app_id); - - SECTION("jwt happy path") { - bool processed = false; - - std::shared_ptr user = log_in(app, AppCredentials::custom(jwt)); - - app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})}, - [&](auto response, auto error) { - CHECK(error == none); - CHECK(response); - CHECK(*response == true); - processed = true; - }); - CHECK(processed); - processed = false; - app->refresh_custom_data(user, [&](auto) { - processed = true; - }); - CHECK(processed); - auto metadata = user->user_profile(); - auto custom_data = *user->custom_data(); - CHECK(custom_data["name"] == "Not Foo Bar"); - CHECK(metadata["name"] == "Foo Bar"); - } -} - -namespace cf = realm::collection_fixtures; -TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects, - cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects, - cf::DictionaryOfMixedLinks) -{ - const std::string valid_pk_name = "_id"; - const auto partition = random_string(100); - TestType test_type("collection", "dest"); - Schema schema = {{"source", - {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true}, - {"realm_id", PropertyType::String | PropertyType::Nullable}, - test_type.property()}}, - {"dest", - { - {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true}, - {"realm_id", PropertyType::String | PropertyType::Nullable}, - }}}; - auto server_app_config = minimal_app_config("collections_of_links", schema); - TestAppSession test_session(create_app(server_app_config)); - - auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) { - timed_sleeping_wait_for([&]() -> bool { - r->refresh(); - TableRef dest = r->read_group().get_table(table_name); - size_t cur_count = dest->size(); - return cur_count == count; - }); - }; - auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) { - timed_sleeping_wait_for([&]() -> bool { - r->refresh(); - return test_type.size_of_collection(obj) == count; - }); - }; - - CppContext c; - auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector links = {}) { - r->begin_transaction(); - auto object = Object::create( - c, r, "source", - std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}), - CreatePolicy::ForceCreate); - - for (auto link : links) { - auto& obj = object.get_obj(); - test_type.add_link(obj, link); - } - r->commit_transaction(); - }; - - auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink { - r->begin_transaction(); - auto obj = Object::create( - c, r, "dest", - std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); - return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()}; - }; - - auto require_links_to_match_ids = [&](std::vector links, std::vector expected) { - std::vector actual; - for (auto obj : links) { - actual.push_back(obj.get(valid_pk_name)); - } - std::sort(actual.begin(), actual.end()); - std::sort(expected.begin(), expected.end()); - REQUIRE(actual == expected); - }; - - SECTION("integration testing") { - auto app = test_session.app(); - SyncTestFile config1(app, partition, schema); // uses the current user created above - auto r1 = realm::Realm::get_shared_realm(config1); - Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source")); - - create_user_and_log_in(app); - SyncTestFile config2(app, partition, schema); // uses the user created above - auto r2 = realm::Realm::get_shared_realm(config2); - Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source")); - - constexpr int64_t source_pk = 0; - constexpr int64_t dest_pk_1 = 1; - constexpr int64_t dest_pk_2 = 2; - constexpr int64_t dest_pk_3 = 3; - { // add a container collection with three valid links - REQUIRE(r1_source_objs.size() == 0); - ObjLink dest1 = create_one_dest_object(r1, dest_pk_1); - ObjLink dest2 = create_one_dest_object(r1, dest_pk_2); - ObjLink dest3 = create_one_dest_object(r1, dest_pk_3); - create_one_source_object(r1, source_pk, {dest1, dest2, dest3}); - REQUIRE(r1_source_objs.size() == 1); - REQUIRE(r1_source_objs.get(0).get(valid_pk_name) == source_pk); - REQUIRE(r1_source_objs.get(0).get("realm_id") == partition); - require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3}); - } - - size_t expected_coll_size = 3; - std::vector remaining_dest_object_ids; - { // erase one of the destination objects - wait_for_num_objects_to_equal(r2, "class_source", 1); - wait_for_num_objects_to_equal(r2, "class_dest", 3); - REQUIRE(r2_source_objs.size() == 1); - REQUIRE(r2_source_objs.get(0).get(valid_pk_name) == source_pk); - REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3); - auto linked_objects = test_type.get_links(r2_source_objs.get(0)); - require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3}); - r2->begin_transaction(); - linked_objects[0].remove(); - r2->commit_transaction(); - remaining_dest_object_ids = {linked_objects[1].template get(valid_pk_name), - linked_objects[2].template get(valid_pk_name)}; - expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3; - REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size); - } - - { // remove a link from the collection - wait_for_num_objects_to_equal(r1, "class_dest", 2); - REQUIRE(r1_source_objs.size() == 1); - REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size); - auto linked_objects = test_type.get_links(r1_source_objs.get(0)); - require_links_to_match_ids(linked_objects, remaining_dest_object_ids); - r1->begin_transaction(); - auto obj = r1_source_objs.get(0); - test_type.remove_link(obj, - ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()}); - r1->commit_transaction(); - --expected_coll_size; - remaining_dest_object_ids = {linked_objects[1].template get(valid_pk_name)}; - REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size); - } - - { // clear the collection - REQUIRE(r2_source_objs.size() == 1); - REQUIRE(r2_source_objs.get(0).get(valid_pk_name) == source_pk); - wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size); - auto linked_objects = test_type.get_links(r2_source_objs.get(0)); - require_links_to_match_ids(linked_objects, remaining_dest_object_ids); - r2->begin_transaction(); - test_type.clear_collection(r2_source_objs.get(0)); - r2->commit_transaction(); - expected_coll_size = 0; - REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size); - } - - { // expect an empty collection - REQUIRE(r1_source_objs.size() == 1); - wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size); - } - } -} - -TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID, - cf::UUID, cf::BoxedOptional, cf::UnboxedOptional, cf::BoxedOptional, - cf::BoxedOptional) -{ - const std::string valid_pk_name = "_id"; - const std::string partition_key_col_name = "partition_key_prop"; - const std::string table_name = "class_partition_test_type"; - auto partition_property = Property(partition_key_col_name, TestType::property_type); - Schema schema = {{Group::table_name_to_class_name(table_name), - { - {valid_pk_name, PropertyType::Int, true}, - partition_property, - }}}; - auto server_app_config = minimal_app_config("partition_types_app_name", schema); - server_app_config.partition_key = partition_property; - TestAppSession test_session(create_app(server_app_config)); - auto app = test_session.app(); - - auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) { - timed_sleeping_wait_for([&]() -> bool { - r->refresh(); - TableRef dest = r->read_group().get_table(table_name); - size_t cur_count = dest->size(); - return cur_count == count; - }); - }; - using T = typename TestType::Type; - CppContext c; - auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) { - r->begin_transaction(); - auto object = Object::create( - c, r, Group::table_name_to_class_name(table_name), - std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); - }; - - auto get_bson = [](T val) -> bson::Bson { - if constexpr (std::is_same_v) { - return val.is_null() ? bson::Bson(util::none) : bson::Bson(val); - } - else if constexpr (TestType::is_optional) { - return val ? bson::Bson(*val) : bson::Bson(util::none); - } - else { - return bson::Bson(val); - } - }; - - SECTION("can round trip an object") { - auto values = TestType::values(); - auto user1 = app->current_user(); - create_user_and_log_in(app); - auto user2 = app->current_user(); - REQUIRE(user1); - REQUIRE(user2); - REQUIRE(user1 != user2); - for (T partition_value : values) { - SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above - auto r1 = realm::Realm::get_shared_realm(config1); - Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name)); - - SyncTestFile config2(user2, get_bson(partition_value), schema); // uses the user created above - auto r2 = realm::Realm::get_shared_realm(config2); - Results r2_source_objs = realm::Results(r2, r2->read_group().get_table(table_name)); - - const int64_t pk_value = random_int(); - { - REQUIRE(r1_source_objs.size() == 0); - create_object(r1, pk_value, TestType::to_any(partition_value)); - REQUIRE(r1_source_objs.size() == 1); - REQUIRE(r1_source_objs.get(0).get(partition_key_col_name) == partition_value); - REQUIRE(r1_source_objs.get(0).get(valid_pk_name) == pk_value); - } - { - wait_for_num_objects_to_equal(r2, table_name, 1); - REQUIRE(r2_source_objs.size() == 1); - REQUIRE(r2_source_objs.size() == 1); - REQUIRE(r2_source_objs.get(0).get(partition_key_col_name) == partition_value); - REQUIRE(r2_source_objs.get(0).get(valid_pk_name) == pk_value); - } - } - } -} - -TEST_CASE("app: full-text compatible with sync", "[sync][app][baas]") { - const std::string valid_pk_name = "_id"; - - Schema schema{ - {"TopLevel", - { - {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, - {"full_text", Property::IsFulltextIndexed{true}}, - }}, - }; - - auto server_app_config = minimal_app_config("full_text", schema); - auto app_session = create_app(server_app_config); - const auto partition = random_string(100); - TestAppSession test_session(app_session, nullptr); - SyncTestFile config(test_session.app(), partition, schema); - SharedRealm realm; - SECTION("sync open") { - INFO("realm opened without async open"); - realm = Realm::get_shared_realm(config); - } - SECTION("async open") { - INFO("realm opened with async open"); - auto async_open_task = Realm::get_synchronized_realm(config); - - auto [realm_promise, realm_future] = util::make_promise_future(); - async_open_task->start( - [promise = std::move(realm_promise)](ThreadSafeReference ref, std::exception_ptr ouch) mutable { - if (ouch) { - try { - std::rethrow_exception(ouch); - } - catch (...) { - promise.set_error(exception_to_status()); - } - } - else { - promise.emplace_value(std::move(ref)); - } - }); - - realm = Realm::get_shared_realm(std::move(realm_future.get())); - } - - CppContext c(realm); - auto obj_id_1 = ObjectId::gen(); - auto obj_id_2 = ObjectId::gen(); - realm->begin_transaction(); - Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}})); - Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}})); - realm->commit_transaction(); - - auto table = realm->read_group().get_table("class_TopLevel"); - REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext); - Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world")); - REQUIRE(world_results.size() == 1); - REQUIRE(world_results.get(0).get_primary_key() == Mixed{obj_id_1}); -} - -#endif // REALM_ENABLE_AUTH_TESTS - -TEST_CASE("app: custom error handling", "[sync][app][custom errors]") { - class CustomErrorTransport : public GenericNetworkTransport { - public: - CustomErrorTransport(int code, const std::string& message) - : m_code(code) - , m_message(message) - { - } - - void send_request_to_server(const Request&, util::UniqueFunction&& completion) override - { - completion(Response{0, m_code, HttpHeaders(), m_message}); - } - - private: - int m_code; - std::string m_message; - }; - - SECTION("custom code and message is sent back") { - TestSyncManager::Config config; - config.transport = std::make_shared(1001, "Boom!"); - TestSyncManager tsm(config); - auto error = failed_log_in(tsm.app()); - CHECK(error.is_custom_error()); - CHECK(*error.additional_status_code == 1001); - CHECK(error.reason() == "Boom!"); - } -} - -static const std::string profile_0_name = "Ursus americanus Ursus boeckhi"; -static const std::string profile_0_first_name = "Ursus americanus"; -static const std::string profile_0_last_name = "Ursus boeckhi"; -static const std::string profile_0_email = "Ursus ursinus"; -static const std::string profile_0_picture_url = "Ursus malayanus"; -static const std::string profile_0_gender = "Ursus thibetanus"; -static const std::string profile_0_birthday = "Ursus americanus"; -static const std::string profile_0_min_age = "Ursus maritimus"; -static const std::string profile_0_max_age = "Ursus arctos"; - -static const nlohmann::json profile_0 = { - {"name", profile_0_name}, {"first_name", profile_0_first_name}, {"last_name", profile_0_last_name}, - {"email", profile_0_email}, {"picture_url", profile_0_picture_url}, {"gender", profile_0_gender}, - {"birthday", profile_0_birthday}, {"min_age", profile_0_min_age}, {"max_age", profile_0_max_age}}; - -static nlohmann::json user_json(std::string access_token, std::string user_id = random_string(15)) -{ - return {{"access_token", access_token}, - {"refresh_token", access_token}, - {"user_id", user_id}, - {"device_id", "Panda Bear"}}; -} - -static nlohmann::json user_profile_json(std::string user_id = random_string(15), - std::string identity_0_id = "Ursus arctos isabellinus", - std::string identity_1_id = "Ursus arctos horribilis", - std::string provider_type = "anon-user") -{ - return {{"user_id", user_id}, - {"identities", - {{{"id", identity_0_id}, {"provider_type", provider_type}}, - {{"id", identity_1_id}, {"provider_type", "lol_wut"}}}}, - {"data", profile_0}}; -} - -// MARK: - Unit Tests - -static TestSyncManager::Config get_config() -{ - return get_config(instance_of); -} - -static const std::string good_access_token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2RldklkIjoi" - "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU0M2Rk" - "Y2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s"; - -static const std::string good_access_token2 = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - "eyJleHAiOjE1ODkzMDE3MjAsImlhdCI6MTU4NDExODcyMCwiaXNzIjoiNWU2YmJiYzBhNmI3ZGZkM2UyNTA0OGI3Iiwic3RpdGNoX2RldklkIjoi" - "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU2YmJi" - "YzBhNmI3ZGZkM2UyNTA0OGIzIiwidHlwIjoiYWNjZXNzIn0.eSX4QMjIOLbdOYOPzQrD_racwLUk1HGFgxtx2a34k80"; - -static const std::string bad_access_token = "lolwut"; -static const std::string dummy_device_id = "123400000000000000000000"; - -TEST_CASE("subscribable unit tests", "[sync][app]") { - struct Foo : public Subscribable { - void event() - { - emit_change_to_subscribers(*this); - } - }; - - auto foo = Foo(); - - SECTION("subscriber receives events") { - auto event_count = 0; - auto token = foo.subscribe([&event_count](auto&) { - event_count++; - }); - - foo.event(); - foo.event(); - foo.event(); - - CHECK(event_count == 3); - } - - SECTION("subscriber can unsubscribe") { - auto event_count = 0; - auto token = foo.subscribe([&event_count](auto&) { - event_count++; - }); - - foo.event(); - CHECK(event_count == 1); - - foo.unsubscribe(token); - foo.event(); - CHECK(event_count == 1); - } - - SECTION("subscriber is unsubscribed on dtor") { - auto event_count = 0; - { - auto token = foo.subscribe([&event_count](auto&) { - event_count++; - }); - - foo.event(); - CHECK(event_count == 1); - } - foo.event(); - CHECK(event_count == 1); - } - - SECTION("multiple subscribers receive events") { - auto event_count = 0; - { - auto token1 = foo.subscribe([&event_count](auto&) { - event_count++; - }); - auto token2 = foo.subscribe([&event_count](auto&) { - event_count++; - }); - - foo.event(); - CHECK(event_count == 2); - } - foo.event(); - CHECK(event_count == 2); - } -} - -TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { - auto config = get_config(); - static_cast(config.transport.get())->set_profile(profile_0); - - SECTION("login_anonymous good") { - UnitTestTransport::access_token = good_access_token; - config.base_path = util::make_temp_dir(); - config.should_teardown_test_directory = false; - { - TestSyncManager tsm(config); - auto app = tsm.app(); - - auto user = log_in(app); - - REQUIRE(user->identities().size() == 1); - CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); - SyncUserProfile user_profile = user->user_profile(); - - CHECK(user_profile.name() == profile_0_name); - CHECK(user_profile.first_name() == profile_0_first_name); - CHECK(user_profile.last_name() == profile_0_last_name); - CHECK(user_profile.email() == profile_0_email); - CHECK(user_profile.picture_url() == profile_0_picture_url); - CHECK(user_profile.gender() == profile_0_gender); - CHECK(user_profile.birthday() == profile_0_birthday); - CHECK(user_profile.min_age() == profile_0_min_age); - CHECK(user_profile.max_age() == profile_0_max_age); - } - App::clear_cached_apps(); - // assert everything is stored properly between runs - { - TestSyncManager tsm(config); - auto app = tsm.app(); - REQUIRE(app->all_users().size() == 1); - auto user = app->all_users()[0]; - REQUIRE(user->identities().size() == 1); - CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); - SyncUserProfile user_profile = user->user_profile(); - - CHECK(user_profile.name() == profile_0_name); - CHECK(user_profile.first_name() == profile_0_first_name); - CHECK(user_profile.last_name() == profile_0_last_name); - CHECK(user_profile.email() == profile_0_email); - CHECK(user_profile.picture_url() == profile_0_picture_url); - CHECK(user_profile.gender() == profile_0_gender); - CHECK(user_profile.birthday() == profile_0_birthday); - CHECK(user_profile.min_age() == profile_0_min_age); - CHECK(user_profile.max_age() == profile_0_max_age); - } - } - - SECTION("login_anonymous bad") { - struct transport : UnitTestTransport { - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - completion({200, 0, {}, user_json(bad_access_token).dump()}); - } - else { - UnitTestTransport::send_request_to_server(request, std::move(completion)); - } - } - }; - - config.transport = instance_of; - TestSyncManager tsm(config); - auto error = failed_log_in(tsm.app()); - CHECK(error.reason() == std::string("jwt missing parts")); - CHECK(error.code_string() == "BadToken"); - CHECK(error.is_json_error()); - CHECK(error.code() == ErrorCodes::BadToken); - } - - SECTION("login_anonynous multiple users") { - UnitTestTransport::access_token = good_access_token; - config.base_path = util::make_temp_dir(); - config.should_teardown_test_directory = false; - TestSyncManager tsm(config); - auto app = tsm.app(); - - auto user1 = log_in(app); - auto user2 = log_in(app, AppCredentials::anonymous(false)); - CHECK(user1 != user2); - } -} - -TEST_CASE("app: UserAPIKeyProviderClient unit_tests", "[sync][app][user][api key]") { - TestSyncManager sync_manager(get_config(), {}); - auto app = sync_manager.app(); - auto client = app->provider_client(); - - std::shared_ptr logged_in_user = - app->backing_store()->get_user("userid", good_access_token, good_access_token, dummy_device_id); - bool processed = false; - ObjectId obj_id(UnitTestTransport::api_key_id.c_str()); - - SECTION("create api key") { - client.create_api_key(UnitTestTransport::api_key_name, logged_in_user, - [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.disabled == false); - CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id); - CHECK(user_api_key.key == UnitTestTransport::api_key); - CHECK(user_api_key.name == UnitTestTransport::api_key_name); - }); - } - - SECTION("fetch api key") { - client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_key.disabled == false); - CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id); - CHECK(user_api_key.name == UnitTestTransport::api_key_name); - }); - } - - SECTION("fetch api keys") { - client.fetch_api_keys(logged_in_user, - [&](std::vector user_api_keys, Optional error) { - REQUIRE_FALSE(error); - CHECK(user_api_keys.size() == 2); - for (auto user_api_key : user_api_keys) { - CHECK(user_api_key.disabled == false); - CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id); - CHECK(user_api_key.name == UnitTestTransport::api_key_name); - } - processed = true; - }); - CHECK(processed); - } -} - - -TEST_CASE("app: user_semantics", "[sync][app][user]") { - TestSyncManager tsm(get_config(), {}); - auto app = tsm.app(); - - const auto login_user_email_pass = [=] { - return log_in(app, AppCredentials::username_password("bob", "thompson")); - }; - const auto login_user_anonymous = [=] { - return log_in(app, AppCredentials::anonymous()); - }; - - CHECK(!app->current_user()); - - int event_processed = 0; - auto token = app->subscribe([&event_processed](auto&) { - event_processed++; - }); - - SECTION("current user is populated") { - const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); - CHECK(event_processed == 1); - } - - SECTION("current user is updated on login") { - const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); - const auto user2 = login_user_email_pass(); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1->identity() != user2->identity()); - CHECK(event_processed == 2); - } - - SECTION("current user is updated to last used user on logout") { - const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - - const auto user2 = login_user_email_pass(); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1 != user2); - - // should reuse existing session - const auto user3 = login_user_anonymous(); - CHECK(user3 == user1); - - auto user_events_processed = 0; - auto _ = user3->subscribe([&user_events_processed](auto&) { - user_events_processed++; - }); - - app->log_out([](auto) {}); - CHECK(user_events_processed == 1); - - CHECK(app->current_user()->identity() == user2->identity()); - - CHECK(app->all_users().size() == 1); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - - CHECK(event_processed == 4); - } - - SECTION("anon users are removed on logout") { - const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - - const auto user2 = login_user_anonymous(); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - CHECK(app->all_users().size() == 1); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1->identity() == user2->identity()); - - app->log_out([](auto) {}); - CHECK(app->all_users().size() == 0); - - CHECK(event_processed == 3); - } - - SECTION("logout user") { - auto user1 = login_user_email_pass(); - auto user2 = login_user_anonymous(); - - // Anonymous users are special - app->log_out(user2, [](Optional error) { - REQUIRE_FALSE(error); - }); - CHECK(user2->state() == SyncUser::State::Removed); - - // Other users can be LoggedOut - app->log_out(user1, [](Optional error) { - REQUIRE_FALSE(error); - }); - CHECK(user1->state() == SyncUser::State::LoggedOut); - - // Logging out already logged out users, does nothing - app->log_out(user1, [](Optional error) { - REQUIRE_FALSE(error); - }); - CHECK(user1->state() == SyncUser::State::LoggedOut); - - app->log_out(user2, [](Optional error) { - REQUIRE_FALSE(error); - }); - CHECK(user2->state() == SyncUser::State::Removed); - - CHECK(event_processed == 4); - } - - SECTION("unsubscribed observers no longer process events") { - app->unsubscribe(token); - - const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - - const auto user2 = login_user_anonymous(); - CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); - CHECK(app->all_users().size() == 1); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1->identity() == user2->identity()); - - app->log_out([](auto) {}); - CHECK(app->all_users().size() == 0); - - CHECK(event_processed == 0); - } -} - -struct ErrorCheckingTransport : public GenericNetworkTransport { - ErrorCheckingTransport(Response* r) - : m_response(r) - { - } - void send_request_to_server(const Request&, util::UniqueFunction&& completion) override - { - completion(Response(*m_response)); - } - -private: - Response* m_response; -}; - -TEST_CASE("app: response error handling", "[sync][app]") { - std::string response_body = nlohmann::json({{"access_token", good_access_token}, - {"refresh_token", good_access_token}, - {"user_id", "Brown Bear"}, - {"device_id", "Panda Bear"}}) - .dump(); - - Response response{200, 0, {{"Content-Type", "text/plain"}}, response_body}; - - TestSyncManager tsm(get_config(std::make_shared(&response))); - auto app = tsm.app(); - - SECTION("http 404") { - response.http_status_code = 404; - auto error = failed_log_in(app); - CHECK(!error.is_json_error()); - CHECK(!error.is_custom_error()); - CHECK(!error.is_service_error()); - CHECK(error.is_http_error()); - CHECK(*error.additional_status_code == 404); - CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos); - } - SECTION("http 500") { - response.http_status_code = 500; - auto error = failed_log_in(app); - CHECK(!error.is_json_error()); - CHECK(!error.is_custom_error()); - CHECK(!error.is_service_error()); - CHECK(error.is_http_error()); - CHECK(*error.additional_status_code == 500); - CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos); - CHECK(error.link_to_server_logs.empty()); - } - - SECTION("custom error code") { - response.custom_status_code = 42; - response.body = "Custom error message"; - auto error = failed_log_in(app); - CHECK(!error.is_http_error()); - CHECK(!error.is_json_error()); - CHECK(!error.is_service_error()); - CHECK(error.is_custom_error()); - CHECK(*error.additional_status_code == 42); - CHECK(error.reason() == std::string("Custom error message")); - CHECK(error.link_to_server_logs.empty()); - } - - SECTION("session error code") { - response.headers = HttpHeaders{{"Content-Type", "application/json"}}; - response.http_status_code = 400; - response.body = nlohmann::json({{"error_code", "MongoDBError"}, - {"error", "a fake MongoDB error message!"}, - {"access_token", good_access_token}, - {"refresh_token", good_access_token}, - {"user_id", "Brown Bear"}, - {"device_id", "Panda Bear"}, - {"link", "http://...whatever the server passes us"}}) - .dump(); - auto error = failed_log_in(app); - CHECK(!error.is_http_error()); - CHECK(!error.is_json_error()); - CHECK(!error.is_custom_error()); - CHECK(error.is_service_error()); - CHECK(error.code() == ErrorCodes::MongoDBError); - CHECK(error.reason() == std::string("a fake MongoDB error message!")); - CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us")); - } - - SECTION("json error code") { - response.body = "this: is not{} a valid json body!"; - auto error = failed_log_in(app); - CHECK(!error.is_http_error()); - CHECK(error.is_json_error()); - CHECK(!error.is_custom_error()); - CHECK(!error.is_service_error()); - CHECK(error.code() == ErrorCodes::MalformedJson); - CHECK(error.reason() == - std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error " - "while parsing value - invalid literal; last read: 'th'")); - CHECK(error.code_string() == "MalformedJson"); - } -} - -TEST_CASE("app: switch user", "[sync][app][user]") { - TestSyncManager tsm(get_config(), {}); - auto app = tsm.app(); - - bool processed = false; - - SECTION("switch user expect success") { - CHECK(app->backing_store()->all_users().size() == 0); - - // Log in user 1 - auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password")); - CHECK(app->backing_store()->get_current_user() == user_a); - - // Log in user 2 - auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password")); - CHECK(app->backing_store()->get_current_user() == user_b); - - CHECK(app->backing_store()->all_users().size() == 2); - - auto user1 = app->switch_user(user_a); - CHECK(user1 == user_a); - - CHECK(app->backing_store()->get_current_user() == user_a); - - auto user2 = app->switch_user(user_b); - CHECK(user2 == user_b); - - CHECK(app->backing_store()->get_current_user() == user_b); - processed = true; - CHECK(processed); - } - - SECTION("cannot switch to a logged out but not removed user") { - CHECK(app->backing_store()->all_users().size() == 0); - - // Log in user 1 - auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password")); - CHECK(app->backing_store()->get_current_user() == user_a); - - app->log_out([&](Optional error) { - REQUIRE_FALSE(error); - }); - - CHECK(app->backing_store()->get_current_user() == nullptr); - CHECK(user_a->state() == SyncUser::State::LoggedOut); - - // Log in user 2 - auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password")); - CHECK(app->backing_store()->get_current_user() == user_b); - CHECK(app->backing_store()->all_users().size() == 2); - - REQUIRE_THROWS_AS(app->switch_user(user_a), AppError); - CHECK(app->backing_store()->get_current_user() == user_b); - } -} - -TEST_CASE("app: remove anonymous user", "[sync][app][user]") { - TestSyncManager tsm(get_config(), {}); - auto app = tsm.app(); - - SECTION("remove user expect success") { - CHECK(app->backing_store()->all_users().size() == 0); - - // Log in user 1 - auto user_a = log_in(app); - CHECK(user_a->state() == SyncUser::State::LoggedIn); - - app->log_out(user_a, [&](Optional error) { - REQUIRE_FALSE(error); - // a logged out anon user will be marked as Removed, not LoggedOut - CHECK(user_a->state() == SyncUser::State::Removed); - }); - CHECK(app->backing_store()->all_users().empty()); - - app->remove_user(user_a, [&](Optional error) { - CHECK(error->reason() == "User has already been removed"); - CHECK(app->backing_store()->all_users().size() == 0); - }); - - // Log in user 2 - auto user_b = log_in(app); - CHECK(app->backing_store()->get_current_user() == user_b); - CHECK(user_b->state() == SyncUser::State::LoggedIn); - CHECK(app->backing_store()->all_users().size() == 1); - - app->remove_user(user_b, [&](Optional error) { - REQUIRE_FALSE(error); - CHECK(app->backing_store()->all_users().size() == 0); - }); - - CHECK(app->backing_store()->get_current_user() == nullptr); - - // check both handles are no longer valid - CHECK(user_a->state() == SyncUser::State::Removed); - CHECK(user_b->state() == SyncUser::State::Removed); - } -} - -TEST_CASE("app: remove user with credentials", "[sync][app][user]") { - TestSyncManager tsm(get_config(), {}); - auto app = tsm.app(); - - SECTION("log in, log out and remove") { - CHECK(app->backing_store()->all_users().size() == 0); - CHECK(app->backing_store()->get_current_user() == nullptr); - - auto user = log_in(app, AppCredentials::username_password("email", "pass")); - - CHECK(user->state() == SyncUser::State::LoggedIn); - - app->log_out(user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - - CHECK(user->state() == SyncUser::State::LoggedOut); - - app->remove_user(user, [&](Optional error) { - REQUIRE_FALSE(error); - }); - CHECK(app->backing_store()->all_users().size() == 0); - - Optional error; - app->remove_user(user, [&](Optional err) { - error = err; - }); - CHECK(error->code() > 0); - CHECK(app->backing_store()->all_users().size() == 0); - CHECK(user->state() == SyncUser::State::Removed); - } -} - -TEST_CASE("app: link_user", "[sync][app][user]") { - TestSyncManager tsm(get_config(), {}); - auto app = tsm.app(); - - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = random_string(10); - - auto custom_credentials = AppCredentials::facebook("a_token"); - auto email_pass_credentials = AppCredentials::username_password(email, password); - - auto sync_user = log_in(app, email_pass_credentials); - REQUIRE(sync_user->identities().size() == 2); - CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword); - - SECTION("successful link") { - bool processed = false; - app->link_user(sync_user, custom_credentials, [&](std::shared_ptr user, Optional error) { - REQUIRE_FALSE(error); - REQUIRE(user); - CHECK(user->identity() == sync_user->identity()); - processed = true; - }); - CHECK(processed); - } - - SECTION("link_user should fail when logged out") { - app->log_out([&](Optional error) { - REQUIRE_FALSE(error); - }); - - bool processed = false; - app->link_user(sync_user, custom_credentials, [&](std::shared_ptr user, Optional error) { - CHECK(error->reason() == "The specified user is not logged in."); - CHECK(!user); - processed = true; - }); - CHECK(processed); - } -} - -TEST_CASE("app: auth providers", "[sync][app][user]") { - SECTION("auth providers facebook") { - auto credentials = AppCredentials::facebook("a_token"); - CHECK(credentials.provider() == AuthProvider::FACEBOOK); - CHECK(credentials.provider_as_string() == IdentityProviderFacebook); - CHECK(credentials.serialize_as_bson() == - bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}}); - } - - SECTION("auth providers anonymous") { - auto credentials = AppCredentials::anonymous(); - CHECK(credentials.provider() == AuthProvider::ANONYMOUS); - CHECK(credentials.provider_as_string() == IdentityProviderAnonymous); - CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}}); - } - - SECTION("auth providers anonymous no reuse") { - auto credentials = AppCredentials::anonymous(false); - CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE); - CHECK(credentials.provider_as_string() == IdentityProviderAnonymous); - CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}}); - } - - SECTION("auth providers google authCode") { - auto credentials = AppCredentials::google(AuthCode("a_token")); - CHECK(credentials.provider() == AuthProvider::GOOGLE); - CHECK(credentials.provider_as_string() == IdentityProviderGoogle); - CHECK(credentials.serialize_as_bson() == - bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}}); - } - - SECTION("auth providers google idToken") { - auto credentials = AppCredentials::google(IdToken("a_token")); - CHECK(credentials.provider() == AuthProvider::GOOGLE); - CHECK(credentials.provider_as_string() == IdentityProviderGoogle); - CHECK(credentials.serialize_as_bson() == - bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}}); - } - - SECTION("auth providers apple") { - auto credentials = AppCredentials::apple("a_token"); - CHECK(credentials.provider() == AuthProvider::APPLE); - CHECK(credentials.provider_as_string() == IdentityProviderApple); - CHECK(credentials.serialize_as_bson() == - bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}}); - } - - SECTION("auth providers custom") { - auto credentials = AppCredentials::custom("a_token"); - CHECK(credentials.provider() == AuthProvider::CUSTOM); - CHECK(credentials.provider_as_string() == IdentityProviderCustom); - CHECK(credentials.serialize_as_bson() == - bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}}); - } - - SECTION("auth providers username password") { - auto credentials = AppCredentials::username_password("user", "pass"); - CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD); - CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword); - CHECK(credentials.serialize_as_bson() == - bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}}); - } - - SECTION("auth providers function") { - bson::BsonDocument function_params{{"name", "mongo"}}; - auto credentials = AppCredentials::function(function_params); - CHECK(credentials.provider() == AuthProvider::FUNCTION); - CHECK(credentials.provider_as_string() == IdentityProviderFunction); - CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}}); - } - - SECTION("auth providers api key") { - auto credentials = AppCredentials::api_key("a key"); - CHECK(credentials.provider() == AuthProvider::API_KEY); - CHECK(credentials.provider_as_string() == IdentityProviderAPIKey); - CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}}); - CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY); - } -} - -TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") { - auto setup_user = [](std::shared_ptr app) { - if (app->backing_store()->get_current_user()) { - return; - } - app->backing_store()->get_user("a_user_id", good_access_token, good_access_token, dummy_device_id); - }; - - SECTION("refresh custom data happy path") { - static bool session_route_hit = false; - - struct transport : UnitTestTransport { - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/session") != std::string::npos) { - session_route_hit = true; - nlohmann::json json{{"access_token", good_access_token}}; - completion({200, 0, {}, json.dump()}); - } - else { - UnitTestTransport::send_request_to_server(request, std::move(completion)); - } - } - }; - - TestSyncManager sync_manager(get_config(instance_of)); - auto app = sync_manager.app(); - setup_user(app); - - bool processed = false; - app->refresh_custom_data(app->backing_store()->get_current_user(), [&](const Optional& error) { - REQUIRE_FALSE(error); - CHECK(session_route_hit); - processed = true; - }); - CHECK(processed); - } - - SECTION("refresh custom data sad path") { - static bool session_route_hit = false; - - struct transport : UnitTestTransport { - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/session") != std::string::npos) { - session_route_hit = true; - nlohmann::json json{{"access_token", bad_access_token}}; - completion({200, 0, {}, json.dump()}); - } - else { - UnitTestTransport::send_request_to_server(request, std::move(completion)); - } - } - }; - - TestSyncManager sync_manager(get_config(instance_of)); - auto app = sync_manager.app(); - setup_user(app); - - bool processed = false; - app->refresh_custom_data(app->backing_store()->get_current_user(), [&](const Optional& error) { - CHECK(error->reason() == "jwt missing parts"); - CHECK(error->code() == ErrorCodes::BadToken); - CHECK(session_route_hit); - processed = true; - }); - CHECK(processed); - } - - SECTION("refresh token ensure flow is correct") { - /* - Expected flow: - Login - this gets access and refresh tokens - Get profile - throw back a 401 error - Refresh token - get a new token for the user - Get profile - get the profile with the new token - */ - - struct transport : GenericNetworkTransport { - bool login_hit = false; - bool get_profile_1_hit = false; - bool get_profile_2_hit = false; - bool refresh_hit = false; - - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - login_hit = true; - completion({200, 0, {}, user_json(good_access_token).dump()}); - } - else if (request.url.find("/profile") != std::string::npos) { - CHECK(login_hit); - - auto item = AppUtils::find_header("Authorization", request.headers); - CHECK(item); - auto access_token = item->second; - // simulated bad token request - if (access_token.find(good_access_token2) != std::string::npos) { - CHECK(login_hit); - CHECK(get_profile_1_hit); - CHECK(refresh_hit); - - get_profile_2_hit = true; - - completion({200, 0, {}, user_profile_json().dump()}); - } - else if (access_token.find(good_access_token) != std::string::npos) { - CHECK(!get_profile_2_hit); - get_profile_1_hit = true; - - completion({401, 0, {}}); - } - } - else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { - CHECK(login_hit); - CHECK(get_profile_1_hit); - CHECK(!get_profile_2_hit); - refresh_hit = true; - - nlohmann::json json{{"access_token", good_access_token2}}; - completion({200, 0, {}, json.dump()}); - } - else if (request.url.find("/location") != std::string::npos) { - CHECK(request.method == HttpMethod::get); - completion({200, - 0, - {}, - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" - "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}); - } - } - }; - - TestSyncManager sync_manager(get_config(instance_of)); - auto app = sync_manager.app(); - setup_user(app); - REQUIRE(log_in(app)); - } -} - -namespace { -class AsyncMockNetworkTransport { -public: - AsyncMockNetworkTransport() - : transport_thread(&AsyncMockNetworkTransport::worker_routine, this) - { - } - - void add_work_item(Response&& response, util::UniqueFunction&& completion) - { - std::lock_guard lk(transport_work_mutex); - transport_work.push_front(ResponseWorkItem{std::move(response), std::move(completion)}); - transport_work_cond.notify_one(); - } - - void add_work_item(util::UniqueFunction cb) - { - std::lock_guard lk(transport_work_mutex); - transport_work.push_front(std::move(cb)); - transport_work_cond.notify_one(); - } - - void mark_complete() - { - std::unique_lock lk(transport_work_mutex); - test_complete = true; - transport_work_cond.notify_one(); - lk.unlock(); - transport_thread.join(); - } - -private: - struct ResponseWorkItem { - Response response; - util::UniqueFunction completion; - }; - - void worker_routine() - { - std::unique_lock lk(transport_work_mutex); - for (;;) { - transport_work_cond.wait(lk, [&] { - return test_complete || !transport_work.empty(); - }); - - if (!transport_work.empty()) { - auto work_item = std::move(transport_work.back()); - transport_work.pop_back(); - lk.unlock(); - - mpark::visit(util::overload{[](ResponseWorkItem& work_item) { - work_item.completion(std::move(work_item.response)); - }, - [](util::UniqueFunction& cb) { - cb(); - }}, - work_item); - - lk.lock(); - continue; - } - - if (test_complete) { - return; - } - } - } - - std::mutex transport_work_mutex; - std::condition_variable transport_work_cond; - bool test_complete = false; - std::list>> transport_work; - JoiningThread transport_thread; -}; - -} // namespace - -TEST_CASE("app: app destroyed during token refresh", "[sync][app][user][token]") { - AsyncMockNetworkTransport mock_transport_worker; - enum class TestState { unknown, location, login, profile_1, profile_2, refresh_1, refresh_2, refresh_3 }; - struct TestStateBundle { - void advance_to(TestState new_state) - { - std::lock_guard lk(mutex); - state = new_state; - cond.notify_one(); - } - - TestState get() const - { - std::lock_guard lk(mutex); - return state; - } - - void wait_for(TestState new_state) - { - std::unique_lock lk(mutex); - bool failed = !cond.wait_for(lk, std::chrono::seconds(5), [&] { - return state == new_state; - }); - if (failed) { - throw std::runtime_error("wait timed out"); - } - } - - mutable std::mutex mutex; - std::condition_variable cond; - - TestState state = TestState::unknown; - } state; - struct transport : public GenericNetworkTransport { - transport(AsyncMockNetworkTransport& worker, TestStateBundle& state) - : mock_transport_worker(worker) - , state(state) - { - } - - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - CHECK(state.get() == TestState::location); - state.advance_to(TestState::login); - mock_transport_worker.add_work_item( - Response{200, 0, {}, user_json(encode_fake_jwt("access token 1")).dump()}, std::move(completion)); - } - else if (request.url.find("/profile") != std::string::npos) { - // simulated bad token request - auto cur_state = state.get(); - CHECK((cur_state == TestState::refresh_1 || cur_state == TestState::login)); - if (cur_state == TestState::refresh_1) { - state.advance_to(TestState::profile_2); - mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()}, - std::move(completion)); - } - else if (cur_state == TestState::login) { - state.advance_to(TestState::profile_1); - mock_transport_worker.add_work_item(Response{401, 0, {}}, std::move(completion)); - } - } - else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { - if (state.get() == TestState::profile_1) { - state.advance_to(TestState::refresh_1); - nlohmann::json json{{"access_token", encode_fake_jwt("access token 1")}}; - mock_transport_worker.add_work_item(Response{200, 0, {}, json.dump()}, std::move(completion)); - } - else if (state.get() == TestState::profile_2) { - state.advance_to(TestState::refresh_2); - mock_transport_worker.add_work_item(Response{200, 0, {}, "{\"error\":\"too bad, buddy!\"}"}, - std::move(completion)); - } - else { - CHECK(state.get() == TestState::refresh_2); - state.advance_to(TestState::refresh_3); - nlohmann::json json{{"access_token", encode_fake_jwt("access token 2")}}; - mock_transport_worker.add_work_item(Response{200, 0, {}, json.dump()}, std::move(completion)); - } - } - else if (request.url.find("/location") != std::string::npos) { - CHECK(request.method == HttpMethod::get); - CHECK(state.get() == TestState::unknown); - state.advance_to(TestState::location); - mock_transport_worker.add_work_item( - Response{200, - 0, - {}, - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" - "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}, - std::move(completion)); - } - } - - AsyncMockNetworkTransport& mock_transport_worker; - TestStateBundle& state; - }; - TestSyncManager sync_manager(get_config(std::make_shared(mock_transport_worker, state))); - auto app = sync_manager.app(); - - { - auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); - app->log_in_with_credentials(AppCredentials::anonymous(), - [promise = std::move(cur_user_promise)](std::shared_ptr user, - util::Optional error) mutable { - REQUIRE_FALSE(error); - promise.emplace_value(std::move(user)); - }); - - auto cur_user = std::move(cur_user_future).get(); - CHECK(cur_user); - - SyncTestFile config(app->current_user(), bson::Bson("foo")); - // Ignore websocket errors, since sometimes a websocket connection gets started during the test - config.sync_config->error_handler = [](std::shared_ptr session, SyncError error) mutable { - // Ignore these errors, since there's not really an app out there... - // Primarily make sure we don't crash unexpectedly - std::vector expected_errors = {"Bad WebSocket", "Connection Failed", "user has been removed", - "Connection refused", "The user is not logged in"}; - auto expected = - std::find_if(expected_errors.begin(), expected_errors.end(), [error](const char* err_msg) { - return error.status.reason().find(err_msg) != std::string::npos; - }); - if (expected != expected_errors.end()) { - util::format(std::cerr, - "An expected possible WebSocket error was caught during test: 'app destroyed during " - "token refresh': '%1' for '%2'", - error.status, session->path()); - } - else { - std::string err_msg(util::format("An unexpected sync error was caught during test: 'app destroyed " - "during token refresh': '%1' for '%2'", - error.status, session->path())); - std::cerr << err_msg << std::endl; - throw std::runtime_error(err_msg); - } - }; - auto r = Realm::get_shared_realm(config); - auto session = r->sync_session(); - mock_transport_worker.add_work_item([session] { - session->initiate_access_token_refresh(); - }); - } - for (const auto& user : app->all_users()) { - user->log_out(); - } - - timed_wait_for([&] { - return !app->sync_manager()->has_existing_sessions(); - }); - - mock_transport_worker.mark_complete(); -} - -TEST_CASE("app: metadata is persisted between sessions", "[sync][app][metadata]") { - static const auto test_hostname = "proto://host:1234"; - static const auto test_ws_hostname = "wsproto://host:1234"; - - struct transport : UnitTestTransport { - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/location") != std::string::npos) { - CHECK(request.method == HttpMethod::get); - completion({200, - 0, - {}, - nlohmann::json({{"deployment_model", "LOCAL"}, - {"location", "IE"}, - {"hostname", test_hostname}, - {"ws_hostname", test_ws_hostname}}) - .dump()}); - } - else if (request.url.find("functions/call") != std::string::npos) { - REQUIRE(request.url.rfind(test_hostname, 0) != std::string::npos); - } - else { - UnitTestTransport::send_request_to_server(request, std::move(completion)); - } - } - }; - - TestSyncManager::Config config = get_config(instance_of); - config.base_path = util::make_temp_dir(); - config.should_teardown_test_directory = false; - - { - TestSyncManager sync_manager(config, {}); - auto app = sync_manager.app(); - app->log_in_with_credentials(AppCredentials::anonymous(), [](auto, auto error) { - REQUIRE_FALSE(error); - }); - REQUIRE(app->sync_manager()->sync_route().rfind(test_ws_hostname, 0) != std::string::npos); - } - - App::clear_cached_apps(); - config.override_sync_route = false; - config.should_teardown_test_directory = true; - { - TestSyncManager sync_manager(config); - auto app = sync_manager.app(); - REQUIRE(app->sync_manager()->sync_route().rfind(test_ws_hostname, 0) != std::string::npos); - app->call_function("function", {}, [](auto error, auto) { - REQUIRE_FALSE(error); - }); - } -} - -TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { - UnitTestTransport::access_token = good_access_token; - - constexpr uint64_t timeout_ms = 60000; - auto config = get_config(); - config.app_config.default_request_timeout_ms = timeout_ms; - TestSyncManager tsm(config); - auto app = tsm.app(); - - std::shared_ptr user = log_in(app); - - using Headers = decltype(Request().headers); - - const auto url_prefix = "field/api/client/v2.0/app/app_id/functions/call?baas_request="sv; - const auto get_request_args = [&](const Request& req) { - REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix); - auto args = req.url.substr(url_prefix.size()); - if (auto amp = args.find('&'); amp != std::string::npos) { - args.resize(amp); - } - - auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args)); - REQUIRE(!!vec); - auto parsed = bson::parse({vec->data(), vec->size()}); - REQUIRE(parsed.type() == bson::Bson::Type::Document); - auto out = parsed.operator const bson::BsonDocument&(); - CHECK(out.size() == 3); - return out; - }; - - const auto make_request = [&](std::shared_ptr user, auto&&... args) { - auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"}); - CHECK(req.method == HttpMethod::get); - CHECK(req.body == ""); - CHECK(req.headers == Headers{{"Accept", "text/event-stream"}}); - CHECK(req.timeout_ms == timeout_ms); - CHECK(req.uses_refresh_token == false); - - auto req_args = get_request_args(req); - CHECK(req_args["name"] == "func"); - CHECK(req_args["service"] == "svc"); - CHECK(req_args["arguments"] == bson::BsonArray{args...}); - - return req; - }; - - SECTION("no args") { - auto req = make_request(nullptr); - CHECK(req.url.find('&') == std::string::npos); - } - SECTION("args") { - auto req = make_request(nullptr, "arg1", "arg2"); - CHECK(req.url.find('&') == std::string::npos); - } - SECTION("percent encoding") { - // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded. - auto req = make_request(nullptr, ">>>>>?????"); - - CHECK(req.url.find('&') == std::string::npos); - CHECK(req.url.find("%2B") != std::string::npos); // + (from >) - CHECK(req.url.find("%2F") != std::string::npos); // / (from ?) - CHECK(req.url.find("%3D") != std::string::npos); // = (tail padding) - CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding) - } - SECTION("with user") { - auto req = make_request(user, "arg1", "arg2"); - - auto amp = req.url.find('&'); - REQUIRE(amp != std::string::npos); - auto tail = req.url.substr(amp); - REQUIRE(tail == ("&baas_at=" + user->access_token())); - } -} - -TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") { - SECTION("with empty map") { - auto profile = SyncUserProfile(bson::BsonDocument()); - CHECK(profile.name() == util::none); - CHECK(profile.email() == util::none); - CHECK(profile.picture_url() == util::none); - CHECK(profile.first_name() == util::none); - CHECK(profile.last_name() == util::none); - CHECK(profile.gender() == util::none); - CHECK(profile.birthday() == util::none); - CHECK(profile.min_age() == util::none); - CHECK(profile.max_age() == util::none); - } - SECTION("with full map") { - auto profile = SyncUserProfile(bson::BsonDocument({ - {"first_name", "Jan"}, - {"last_name", "Jaanson"}, - {"name", "Jan Jaanson"}, - {"email", "jan.jaanson@jaanson.com"}, - {"gender", "none"}, - {"birthday", "January 1, 1970"}, - {"min_age", "0"}, - {"max_age", "100"}, - {"picture_url", "some"}, - })); - CHECK(profile.name() == "Jan Jaanson"); - CHECK(profile.email() == "jan.jaanson@jaanson.com"); - CHECK(profile.picture_url() == "some"); - CHECK(profile.first_name() == "Jan"); - CHECK(profile.last_name() == "Jaanson"); - CHECK(profile.gender() == "none"); - CHECK(profile.birthday() == "January 1, 1970"); - CHECK(profile.min_age() == "0"); - CHECK(profile.max_age() == "100"); - } -} - -#if 0 -TEST_CASE("app: app cannot get deallocated during log in", "[sync][app]") { - AsyncMockNetworkTransport mock_transport_worker; - enum class TestState { unknown, location, login, app_deallocated, profile }; - struct TestStateBundle { - void advance_to(TestState new_state) - { - std::lock_guard lk(mutex); - state = new_state; - cond.notify_one(); - } - - TestState get() const - { - std::lock_guard lk(mutex); - return state; - } - - void wait_for(TestState new_state) - { - std::unique_lock lk(mutex); - cond.wait(lk, [&] { - return state == new_state; - }); - } - - mutable std::mutex mutex; - std::condition_variable cond; - - TestState state = TestState::unknown; - } state; - struct transport : public GenericNetworkTransport { - transport(AsyncMockNetworkTransport& worker, TestStateBundle& state) - : mock_transport_worker(worker) - , state(state) - { - } - - void send_request_to_server(const Request& request, util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - state.advance_to(TestState::login); - state.wait_for(TestState::app_deallocated); - mock_transport_worker.add_work_item( - Response{200, 0, {}, user_json(encode_fake_jwt("access token")).dump()}, - std::move(completion)); - } - else if (request.url.find("/profile") != std::string::npos) { - state.advance_to(TestState::profile); - mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()}, - std::move(completion)); - } - else if (request.url.find("/location") != std::string::npos) { - CHECK(request.method == HttpMethod::get); - state.advance_to(TestState::location); - mock_transport_worker.add_work_item( - Response{200, - 0, - {}, - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" - "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}, - std::move(completion)); - } - } - - AsyncMockNetworkTransport& mock_transport_worker; - TestStateBundle& state; - }; - - auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); - auto transporter = std::make_shared(mock_transport_worker, state); - - { - TestSyncManager sync_manager(get_config(transporter)); - auto app = sync_manager.app(); - - app->log_in_with_credentials(AppCredentials::anonymous(), - [promise = std::move(cur_user_promise)](std::shared_ptr user, - util::Optional error) mutable { - REQUIRE_FALSE(error); - promise.emplace_value(std::move(user)); - }); - } - - // At this point the test does not hold any reference to `app`. - state.advance_to(TestState::app_deallocated); - auto cur_user = std::move(cur_user_future).get(); - CHECK(cur_user); - - mock_transport_worker.mark_complete(); -} -#endif - -TEST_CASE("app: user logs out while profile is fetched", "[sync][app][user]") { - AsyncMockNetworkTransport mock_transport_worker; - enum class TestState { unknown, location, login, profile }; - struct TestStateBundle { - void advance_to(TestState new_state) - { - std::lock_guard lk(mutex); - state = new_state; - cond.notify_one(); - } - - TestState get() const - { - std::lock_guard lk(mutex); - return state; - } - - void wait_for(TestState new_state) - { - std::unique_lock lk(mutex); - cond.wait(lk, [&] { - return state == new_state; - }); - } - - mutable std::mutex mutex; - std::condition_variable cond; - - TestState state = TestState::unknown; - } state; - struct transport : public GenericNetworkTransport { - transport(AsyncMockNetworkTransport& worker, TestStateBundle& state, - std::shared_ptr& logged_in_user) - : mock_transport_worker(worker) - , state(state) - , logged_in_user(logged_in_user) - { - } - - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - state.advance_to(TestState::login); - mock_transport_worker.add_work_item( - Response{200, 0, {}, user_json(encode_fake_jwt("access token")).dump()}, std::move(completion)); - } - else if (request.url.find("/profile") != std::string::npos) { - logged_in_user->log_out(); - state.advance_to(TestState::profile); - mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()}, - std::move(completion)); - } - else if (request.url.find("/location") != std::string::npos) { - CHECK(request.method == HttpMethod::get); - state.advance_to(TestState::location); - mock_transport_worker.add_work_item( - Response{200, - 0, - {}, - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" - "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}, - std::move(completion)); - } - } - - AsyncMockNetworkTransport& mock_transport_worker; - TestStateBundle& state; - std::shared_ptr& logged_in_user; - }; - - std::shared_ptr logged_in_user; - auto transporter = std::make_shared(mock_transport_worker, state, logged_in_user); - - TestSyncManager sync_manager(get_config(transporter)); - auto app = sync_manager.app(); - - logged_in_user = app->backing_store()->get_user("userid", good_access_token, good_access_token, dummy_device_id); - auto custom_credentials = AppCredentials::facebook("a_token"); - auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); - - app->link_user(logged_in_user, custom_credentials, - [promise = std::move(cur_user_promise)](std::shared_ptr user, - util::Optional error) mutable { - REQUIRE_FALSE(error); - promise.emplace_value(std::move(user)); - }); - - auto cur_user = std::move(cur_user_future).get(); - CHECK(state.get() == TestState::profile); - CHECK(cur_user); - CHECK(cur_user == logged_in_user); - - mock_transport_worker.mark_complete(); -} - -TEST_CASE("app: shared instances", "[sync][app]") { - App::Config base_config; - set_app_config_defaults(base_config, instance_of); - - SyncClientConfig sync_config; - sync_config.backing_store_config.metadata_mode = app::BackingStoreConfig::MetadataMode::NoMetadata; - sync_config.backing_store_config.base_file_path = util::make_temp_dir() + random_string(10); - util::try_make_dir(sync_config.backing_store_config.base_file_path); - - auto config1 = base_config; - config1.app_id = "app1"; - - auto config2 = base_config; - config2.app_id = "app1"; - config2.base_url = "https://realm.mongodb.com"; // equivalent to default_base_url - - auto config3 = base_config; - config3.app_id = "app2"; - - auto config4 = base_config; - config4.app_id = "app2"; - config4.base_url = "http://localhost:9090"; - - // should all point to same underlying app - auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1, sync_config); - auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1, sync_config); - auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url); - auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2, sync_config); - auto app1_5 = App::get_cached_app(config1.app_id); - - CHECK(app1_1 == app1_2); - CHECK(app1_1 == app1_3); - CHECK(app1_1 == app1_4); - CHECK(app1_1 == app1_5); - - // config3 and config4 should point to different apps - auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3, sync_config); - auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url); - auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4, sync_config); - auto app2_4 = App::get_cached_app(config3.app_id); - auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url"); - - CHECK(app2_1 == app2_2); - CHECK(app2_1 != app2_3); - CHECK(app2_4 != nullptr); - CHECK(app2_5 == nullptr); - - CHECK(app1_1 != app2_1); - CHECK(app1_1 != app2_3); - CHECK(app1_1 != app2_4); -} diff --git a/test/object-store/sync/app_non_sync_services.cpp b/test/object-store/sync/app_non_sync_services.cpp new file mode 100644 index 00000000000..338e85f12c7 --- /dev/null +++ b/test/object-store/sync/app_non_sync_services.cpp @@ -0,0 +1,2803 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "collection_fixtures.hpp" +#include "util/sync/baas_admin_api.hpp" +#include "util/sync/sync_test_utils.hpp" +#include "util/unit_test_transport.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace realm; +using namespace realm::app; +using util::any_cast; +using util::Optional; + +using namespace std::string_view_literals; +using namespace std::literals::string_literals; + +#if REALM_ENABLE_AUTH_TESTS + +#include + +static std::string create_jwt(const std::string& appId) +{ + nlohmann::json header = {{"alg", "HS256"}, {"typ", "JWT"}}; + nlohmann::json payload = {{"aud", appId}, {"sub", "someUserId"}, {"exp", 1961896476}}; + + payload["user_data"]["name"] = "Foo Bar"; + payload["user_data"]["occupation"] = "firefighter"; + + payload["my_metadata"]["name"] = "Bar Foo"; + payload["my_metadata"]["occupation"] = "stock analyst"; + + std::string headerStr = header.dump(); + std::string payloadStr = payload.dump(); + + std::string encoded_header; + encoded_header.resize(util::base64_encoded_size(headerStr.length())); + util::base64_encode(headerStr.data(), headerStr.length(), encoded_header.data(), encoded_header.size()); + + std::string encoded_payload; + encoded_payload.resize(util::base64_encoded_size(payloadStr.length())); + util::base64_encode(payloadStr.data(), payloadStr.length(), encoded_payload.data(), encoded_payload.size()); + + // Remove padding characters. + while (encoded_header.back() == '=') + encoded_header.pop_back(); + while (encoded_payload.back() == '=') + encoded_payload.pop_back(); + + std::string jwtPayload = encoded_header + "." + encoded_payload; + + std::array hmac; + unsigned char key[] = "My_very_confidential_secretttttt"; + util::hmac_sha256(util::unsafe_span_cast(jwtPayload), hmac, util::Span(key, 32)); + + std::string signature; + signature.resize(util::base64_encoded_size(hmac.size())); + util::base64_encode(reinterpret_cast(hmac.data()), hmac.size(), signature.data(), signature.size()); + while (signature.back() == '=') + signature.pop_back(); + std::replace(signature.begin(), signature.end(), '+', '-'); + std::replace(signature.begin(), signature.end(), '/', '_'); + + return jwtPayload + "." + signature; +} + +// MARK: - Verify AppError with all error codes +TEST_CASE("app: verify app error codes", "[sync][app][local]") { + auto error_codes = ErrorCodes::get_error_list(); + std::vector> http_status_codes = { + {0, ""}, + {100, "http error code considered fatal: some http error. Informational: 100"}, + {200, ""}, + {300, "http error code considered fatal: some http error. Redirection: 300"}, + {400, "http error code considered fatal: some http error. Client Error: 400"}, + {500, "http error code considered fatal: some http error. Server Error: 500"}, + {600, "http error code considered fatal: some http error. Unknown HTTP Error: 600"}}; + + auto make_http_error = [](std::optional error_code, int http_status = 500, + std::optional error = "some error", + std::optional link = "http://dummy-link/") -> app::Response { + nlohmann::json body; + if (error_code) { + body["error_code"] = *error_code; + } + if (error) { + body["error"] = *error; + } + if (link) { + body["link"] = *link; + } + + return { + http_status, + 0, + {{"Content-Type", "application/json"}}, + body.empty() ? "{}" : body.dump(), + }; + }; + + // Success response + app::Response response = {200, 0, {}, ""}; + auto app_error = AppUtils::check_for_errors(response); + REQUIRE(!app_error); + + // Empty error code + response = make_http_error(""); + app_error = AppUtils::check_for_errors(response); + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::AppUnknownError); + REQUIRE(app_error->code_string() == "AppUnknownError"); + REQUIRE(app_error->server_error.empty()); + REQUIRE(app_error->reason() == "some error"); + REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); + REQUIRE(*app_error->additional_status_code == 500); + + // Missing error code + response = make_http_error(std::nullopt); + app_error = AppUtils::check_for_errors(response); + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::AppUnknownError); + REQUIRE(app_error->code_string() == "AppUnknownError"); + REQUIRE(app_error->server_error.empty()); + REQUIRE(app_error->reason() == "some error"); + REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); + REQUIRE(*app_error->additional_status_code == 500); + + // Missing error code and error message with success http status + response = make_http_error(std::nullopt, 200, std::nullopt); + app_error = AppUtils::check_for_errors(response); + REQUIRE(!app_error); + + for (auto [name, error] : error_codes) { + // All error codes should not cause an exception + if (error != ErrorCodes::HTTPError && error != ErrorCodes::OK) { + response = make_http_error(name); + app_error = AppUtils::check_for_errors(response); + REQUIRE(app_error); + if (ErrorCodes::error_categories(error).test(ErrorCategory::app_error)) { + REQUIRE(app_error->code() == error); + REQUIRE(app_error->code_string() == name); + } + else { + REQUIRE(app_error->code() == ErrorCodes::AppServerError); + REQUIRE(app_error->code_string() == "AppServerError"); + } + REQUIRE(app_error->server_error == name); + REQUIRE(app_error->reason() == "some error"); + REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); + REQUIRE(app_error->additional_status_code); + REQUIRE(*app_error->additional_status_code == 500); + } + } + + response = make_http_error("AppErrorMissing", 404); + app_error = AppUtils::check_for_errors(response); + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::AppServerError); + REQUIRE(app_error->code_string() == "AppServerError"); + REQUIRE(app_error->server_error == "AppErrorMissing"); + REQUIRE(app_error->reason() == "some error"); + REQUIRE(app_error->link_to_server_logs == "http://dummy-link/"); + REQUIRE(app_error->additional_status_code); + REQUIRE(*app_error->additional_status_code == 404); + + // HTTPError with different status values + for (auto [status, message] : http_status_codes) { + response = { + status, + 0, + {}, + "some http error", + }; + app_error = AppUtils::check_for_errors(response); + if (message.empty()) { + REQUIRE(!app_error); + continue; + } + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::HTTPError); + REQUIRE(app_error->code_string() == "HTTPError"); + REQUIRE(app_error->server_error.empty()); + REQUIRE(app_error->reason() == message); + REQUIRE(app_error->link_to_server_logs.empty()); + REQUIRE(app_error->additional_status_code); + REQUIRE(*app_error->additional_status_code == status); + } + + // Missing error code and error message with fatal http status + response = { + 501, + 0, + {}, + "", + }; + app_error = AppUtils::check_for_errors(response); + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::HTTPError); + REQUIRE(app_error->code_string() == "HTTPError"); + REQUIRE(app_error->server_error.empty()); + REQUIRE(app_error->reason() == "http error code considered fatal. Server Error: 501"); + REQUIRE(app_error->link_to_server_logs.empty()); + REQUIRE(app_error->additional_status_code); + REQUIRE(*app_error->additional_status_code == 501); + + // Valid client error code, with body, but no json + app::Response client_response = { + 501, + 0, + {}, + "Some error occurred", + ErrorCodes::BadBsonParse, // client_error_code + }; + app_error = AppUtils::check_for_errors(client_response); + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::BadBsonParse); + REQUIRE(app_error->code_string() == "BadBsonParse"); + REQUIRE(app_error->server_error.empty()); + REQUIRE(app_error->reason() == "Some error occurred"); + REQUIRE(app_error->link_to_server_logs.empty()); + REQUIRE(app_error->additional_status_code); + REQUIRE(*app_error->additional_status_code == 501); + + // Same response with client error code, but no body + client_response.body = ""; + app_error = AppUtils::check_for_errors(client_response); + REQUIRE(app_error); + REQUIRE(app_error->reason() == "client error code value considered fatal"); + + // Valid custom status code, with body, but no json + app::Response custom_response = {501, + 4999, // custom_status_code + {}, + "Some custom error occurred"}; + app_error = AppUtils::check_for_errors(custom_response); + REQUIRE(app_error); + REQUIRE(app_error->code() == ErrorCodes::CustomError); + REQUIRE(app_error->code_string() == "CustomError"); + REQUIRE(app_error->server_error.empty()); + REQUIRE(app_error->reason() == "Some custom error occurred"); + REQUIRE(app_error->link_to_server_logs.empty()); + REQUIRE(app_error->additional_status_code); + REQUIRE(*app_error->additional_status_code == 4999); + + // Same response with custom status code, but no body + custom_response.body = ""; + app_error = AppUtils::check_for_errors(custom_response); + REQUIRE(app_error); + REQUIRE(app_error->reason() == "non-zero custom status code considered fatal"); +} + +// MARK: - Login with Credentials Tests + +TEST_CASE("app: login_with_credentials integration", "[sync][app][user][baas]") { + SECTION("login") { + TestAppSession session; + auto app = session.app(); + app->log_out([](auto) {}); + + int subscribe_processed = 0; + auto token = app->subscribe([&subscribe_processed](auto& app) { + if (!subscribe_processed) { + REQUIRE(app.backing_store()->get_current_user()); + } + else { + REQUIRE_FALSE(app.backing_store()->get_current_user()); + } + subscribe_processed++; + }); + + auto user = log_in(app); + CHECK(!user->device_id().empty()); + CHECK(user->has_device_id()); + + bool processed = false; + app->log_out([&](auto error) { + REQUIRE_FALSE(error); + processed = true; + }); + + CHECK(processed); + CHECK(subscribe_processed == 2); + + app->unsubscribe(token); + } +} + +// MARK: - UsernamePasswordProviderClient Tests + +TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][baas]") { + const std::string base_url = get_base_url(); + AutoVerifiedEmailCredentials creds; + auto email = creds.email; + auto password = creds.password; + + TestAppSession session; + auto app = session.app(); + auto client = app->provider_client(); + + bool processed = false; + + client.register_email(email, password, [&](Optional error) { + CAPTURE(email); + CAPTURE(password); + REQUIRE_FALSE(error); // first registration success + }); + + SECTION("double registration should fail") { + client.register_email(email, password, [&](Optional error) { + // Error returned states the account has already been created + REQUIRE(error); + CHECK(error->reason() == "name already in use"); + CHECK(error->code() == ErrorCodes::AccountNameInUse); + CHECK(!error->link_to_server_logs.empty()); + CHECK(error->link_to_server_logs.find(base_url) != std::string::npos); + processed = true; + }); + CHECK(processed); + } + + SECTION("double registration should fail") { + // the server registration function will reject emails that do not contain "realm_tests_do_autoverify" + std::string email_to_reject = util::format("%1@%2.com", random_string(10), random_string(10)); + client.register_email(email_to_reject, password, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == util::format("failed to confirm user \"%1\"", email_to_reject)); + CHECK(error->code() == ErrorCodes::BadRequest); + processed = true; + }); + CHECK(processed); + } + + SECTION("can login with registered account") { + auto user = log_in(app, creds); + CHECK(user->user_profile().email() == email); + } + + SECTION("cannot login with wrong password") { + app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"), + [&](std::shared_ptr user, Optional error) { + CHECK(!user); + REQUIRE(error); + REQUIRE(error->code() == ErrorCodes::InvalidPassword); + processed = true; + }); + CHECK(processed); + } + + SECTION("confirm user") { + client.confirm_user("a_token", "a_token_id", [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "invalid token data"); + processed = true; + }); + CHECK(processed); + } + + SECTION("resend confirmation email") { + client.resend_confirmation_email(email, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "already confirmed"); + processed = true; + }); + CHECK(processed); + } + + SECTION("reset password invalid tokens") { + client.reset_password(password, "token_sample", "token_id_sample", [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "invalid token data"); + CHECK(!error->link_to_server_logs.empty()); + CHECK(error->link_to_server_logs.find(base_url) != std::string::npos); + processed = true; + }); + CHECK(processed); + } + + SECTION("reset password function success") { + // the imported test app will accept password reset if the password contains "realm_tests_do_reset" via a + // function + std::string accepted_new_password = util::format("realm_tests_do_reset%1", random_string(10)); + client.call_reset_password_function(email, accepted_new_password, {}, [&](Optional error) { + REQUIRE_FALSE(error); + processed = true; + }); + CHECK(processed); + } + + SECTION("reset password function failure") { + std::string rejected_password = util::format("%1", random_string(10)); + client.call_reset_password_function(email, rejected_password, {"foo", "bar"}, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == util::format("failed to reset password for user \"%1\"", email)); + CHECK(error->is_service_error()); + processed = true; + }); + CHECK(processed); + } + + SECTION("reset password function for invalid user fails") { + client.call_reset_password_function(util::format("%1@%2.com", random_string(5), random_string(5)), password, + {"foo", "bar"}, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "user not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::UserNotFound); + processed = true; + }); + CHECK(processed); + } + + SECTION("retry custom confirmation") { + client.retry_custom_confirmation(email, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "already confirmed"); + processed = true; + }); + CHECK(processed); + } + + SECTION("retry custom confirmation for invalid user fails") { + client.retry_custom_confirmation(util::format("%1@%2.com", random_string(5), random_string(5)), + [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "user not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::UserNotFound); + processed = true; + }); + CHECK(processed); + } + + SECTION("log in, remove, log in") { + app->remove_user(app->backing_store()->get_current_user(), [](auto) {}); + CHECK(app->backing_store()->all_users().size() == 0); + CHECK(app->backing_store()->get_current_user() == nullptr); + + auto user = log_in(app, AppCredentials::username_password(email, password)); + CHECK(user->user_profile().email() == email); + CHECK(user->state() == SyncUser::State::LoggedIn); + + app->remove_user(user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + CHECK(user->state() == SyncUser::State::Removed); + + log_in(app, AppCredentials::username_password(email, password)); + CHECK(user->state() == SyncUser::State::Removed); + CHECK(app->backing_store()->get_current_user() != user); + user = app->backing_store()->get_current_user(); + CHECK(user->user_profile().email() == email); + CHECK(user->state() == SyncUser::State::LoggedIn); + + app->remove_user(user, [&](Optional error) { + REQUIRE(!error); + CHECK(app->backing_store()->all_users().size() == 0); + processed = true; + }); + + CHECK(user->state() == SyncUser::State::Removed); + CHECK(processed); + CHECK(app->backing_store()->all_users().size() == 0); + } +} + +// MARK: - UserAPIKeyProviderClient Tests + +TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baas]") { + TestAppSession session; + auto app = session.app(); + auto client = app->provider_client(); + + bool processed = false; + App::UserAPIKey api_key; + + SECTION("api-key") { + std::shared_ptr logged_in_user = app->backing_store()->get_current_user(); + auto api_key_name = util::format("%1", random_string(15)); + client.create_api_key(api_key_name, logged_in_user, + [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.name == api_key_name); + api_key = user_api_key; + }); + + client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.name == api_key_name); + CHECK(user_api_key.id == api_key.id); + }); + + client.fetch_api_keys(logged_in_user, [&](std::vector api_keys, Optional error) { + CHECK(api_keys.size() == 1); + for (auto key : api_keys) { + CHECK(key.id.to_string() == api_key.id.to_string()); + CHECK(api_key.name == api_key_name); + CHECK(key.id == api_key.id); + } + REQUIRE_FALSE(error); + }); + + client.enable_api_key(api_key.id, logged_in_user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.disabled == false); + CHECK(user_api_key.name == api_key_name); + CHECK(user_api_key.id == api_key.id); + }); + + client.disable_api_key(api_key.id, logged_in_user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.disabled == true); + CHECK(user_api_key.name == api_key_name); + }); + + client.delete_api_key(api_key.id, logged_in_user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { + CHECK(user_api_key.name == ""); + CHECK(error); + processed = true; + }); + + CHECK(processed); + } + + SECTION("api-key without a user") { + std::shared_ptr no_user = nullptr; + auto api_key_name = util::format("%1", random_string(15)); + client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + CHECK(user_api_key.name == ""); + }); + + client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + CHECK(user_api_key.name == ""); + }); + + client.fetch_api_keys(no_user, [&](std::vector api_keys, Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + CHECK(api_keys.size() == 0); + }); + + client.enable_api_key(api_key.id, no_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + }); + + client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + CHECK(user_api_key.name == ""); + }); + + client.disable_api_key(api_key.id, no_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + }); + + client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + CHECK(user_api_key.name == ""); + }); + + client.delete_api_key(api_key.id, no_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + }); + + client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional error) { + CHECK(user_api_key.name == ""); + REQUIRE(error); + CHECK(error->is_service_error()); + CHECK(error->reason() == "must authenticate first"); + processed = true; + }); + CHECK(processed); + } + + SECTION("api-key against the wrong user") { + std::shared_ptr first_user = app->backing_store()->get_current_user(); + create_user_and_log_in(app); + std::shared_ptr second_user = app->backing_store()->get_current_user(); + REQUIRE(first_user != second_user); + auto api_key_name = util::format("%1", random_string(15)); + App::UserAPIKey api_key; + App::UserAPIKeyProviderClient provider = app->provider_client(); + + provider.create_api_key(api_key_name, first_user, + [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.name == api_key_name); + api_key = user_api_key; + }); + + provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.name == api_key_name); + CHECK(user_api_key.id.to_string() == user_api_key.id.to_string()); + }); + + provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + CHECK(user_api_key.name == ""); + }); + + provider.fetch_api_keys(first_user, [&](std::vector api_keys, Optional error) { + CHECK(api_keys.size() == 1); + for (auto api_key : api_keys) { + CHECK(api_key.name == api_key_name); + } + REQUIRE_FALSE(error); + }); + + provider.fetch_api_keys(second_user, [&](std::vector api_keys, Optional error) { + CHECK(api_keys.size() == 0); + REQUIRE_FALSE(error); + }); + + provider.enable_api_key(api_key.id, first_user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + provider.enable_api_key(api_key.id, second_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + }); + + provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.disabled == false); + CHECK(user_api_key.name == api_key_name); + }); + + provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(user_api_key.name == ""); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + }); + + provider.disable_api_key(api_key.id, first_user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + provider.disable_api_key(api_key.id, second_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + }); + + provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.disabled == true); + CHECK(user_api_key.name == api_key_name); + }); + + provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE(error); + CHECK(user_api_key.name == ""); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + }); + + provider.delete_api_key(api_key.id, second_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + }); + + provider.delete_api_key(api_key.id, first_user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional error) { + CHECK(user_api_key.name == ""); + REQUIRE(error); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + processed = true; + }); + + provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional error) { + CHECK(user_api_key.name == ""); + REQUIRE(error); + CHECK(error->reason() == "API key not found"); + CHECK(error->is_service_error()); + CHECK(error->code() == ErrorCodes::APIKeyNotFound); + processed = true; + }); + + CHECK(processed); + } +} + +// MARK: - Auth Providers Function Tests + +TEST_CASE("app: auth providers function integration", "[sync][app][user][baas]") { + TestAppSession session; + auto app = session.app(); + + SECTION("auth providers function integration") { + bson::BsonDocument function_params{{"realmCustomAuthFuncUserId", "123456"}}; + auto credentials = AppCredentials::function(function_params); + auto user = log_in(app, credentials); + REQUIRE(user->identities()[0].provider_type == IdentityProviderFunction); + } +} + +// MARK: - Link User Tests + +TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { + TestAppSession session; + auto app = session.app(); + auto user = log_in(app); + + AutoVerifiedEmailCredentials creds; + app->provider_client().register_email(creds.email, creds.password, + [&](Optional error) { + REQUIRE_FALSE(error); + }); + + SECTION("anonymous users are reused before they are linked to an identity") { + REQUIRE(user == log_in(app)); + } + + SECTION("linking a user adds that identity to the user") { + REQUIRE(user->identities().size() == 1); + CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); + + app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { + REQUIRE_FALSE(error); + REQUIRE(user == user2); + REQUIRE(user->identities().size() == 2); + CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); + CHECK(user->identities()[1].provider_type == IdentityProviderUsernamePassword); + }); + } + + SECTION("linking an identity makes the user no longer returned by anonymous logins") { + app->link_user(user, creds, [&](std::shared_ptr, Optional error) { + REQUIRE_FALSE(error); + }); + auto user2 = log_in(app); + REQUIRE(user != user2); + } + + SECTION("existing users are reused when logging in via linked identities") { + app->link_user(user, creds, [](std::shared_ptr, Optional error) { + REQUIRE_FALSE(error); + }); + app->log_out([](auto error) { + REQUIRE_FALSE(error); + }); + REQUIRE(user->state() == SyncUser::State::LoggedOut); + // Should give us the same user instance despite logging in with a + // different identity + REQUIRE(user == log_in(app, creds)); + REQUIRE(user->state() == SyncUser::State::LoggedIn); + } +} + +// MARK: - Delete User Tests + +TEST_CASE("app: delete anonymous user integration", "[sync][app][user][baas]") { + TestAppSession session; + auto app = session.app(); + auto backing_store = app->backing_store(); + + SECTION("delete user expect success") { + CHECK(backing_store->all_users().size() == 1); + + // Log in user 1 + auto user_a = backing_store->get_current_user(); + CHECK(user_a->state() == SyncUser::State::LoggedIn); + app->delete_user(user_a, [&](Optional error) { + REQUIRE_FALSE(error); + // a logged out anon user will be marked as Removed, not LoggedOut + CHECK(user_a->state() == SyncUser::State::Removed); + }); + CHECK(backing_store->all_users().empty()); + CHECK(backing_store->get_current_user() == nullptr); + + app->delete_user(user_a, [&](Optional error) { + CHECK(error->reason() == "User must be logged in to be deleted."); + CHECK(backing_store->all_users().size() == 0); + }); + + // Log in user 2 + auto user_b = log_in(app); + CHECK(backing_store->get_current_user() == user_b); + CHECK(user_b->state() == SyncUser::State::LoggedIn); + CHECK(backing_store->all_users().size() == 1); + + app->delete_user(user_b, [&](Optional error) { + REQUIRE_FALSE(error); + CHECK(backing_store->all_users().size() == 0); + }); + + CHECK(backing_store->get_current_user() == nullptr); + + // check both handles are no longer valid + CHECK(user_a->state() == SyncUser::State::Removed); + CHECK(user_b->state() == SyncUser::State::Removed); + } +} + +TEST_CASE("app: delete user with credentials integration", "[sync][app][user][baas]") { + TestAppSession session; + auto app = session.app(); + auto backing_store = app->backing_store(); + app->remove_user(backing_store->get_current_user(), [](auto) {}); + + SECTION("log in and delete") { + CHECK(backing_store->all_users().size() == 0); + CHECK(backing_store->get_current_user() == nullptr); + + auto credentials = create_user_and_log_in(app); + auto user = backing_store->get_current_user(); + + CHECK(backing_store->get_current_user() == user); + CHECK(user->state() == SyncUser::State::LoggedIn); + app->delete_user(user, [&](Optional error) { + REQUIRE_FALSE(error); + CHECK(app->backing_store()->all_users().size() == 0); + }); + CHECK(user->state() == SyncUser::State::Removed); + CHECK(backing_store->get_current_user() == nullptr); + + app->log_in_with_credentials(credentials, [](std::shared_ptr user, util::Optional error) { + CHECK(!user); + REQUIRE(error); + REQUIRE(error->code() == ErrorCodes::InvalidPassword); + }); + CHECK(backing_store->get_current_user() == nullptr); + + CHECK(backing_store->all_users().size() == 0); + app->delete_user(user, [](Optional err) { + CHECK(err->code() > 0); + }); + + CHECK(backing_store->get_current_user() == nullptr); + CHECK(backing_store->all_users().size() == 0); + CHECK(user->state() == SyncUser::State::Removed); + } +} + +// MARK: - Call Function Tests + +TEST_CASE("app: call function", "[sync][app][function][baas]") { + TestAppSession session; + auto app = session.app(); + + bson::BsonArray toSum(5); + std::iota(toSum.begin(), toSum.end(), static_cast(1)); + const auto checkFn = [](Optional&& sum, Optional&& error) { + REQUIRE(!error); + CHECK(*sum == 15); + }; + app->call_function("sumFunc", toSum, checkFn); + app->call_function(app->backing_store()->get_current_user(), "sumFunc", toSum, checkFn); +} + +// MARK: - Remote Mongo Client Tests + +TEST_CASE("app: remote mongo client", "[sync][app][mongo][baas]") { + TestAppSession session; + auto app = session.app(); + + auto remote_client = app->backing_store()->get_current_user()->mongo_client("BackingDB"); + auto app_session = get_runtime_app_session(); + auto db = remote_client.db(app_session.config.mongo_dbname); + auto dog_collection = db["Dog"]; + auto cat_collection = db["Cat"]; + auto person_collection = db["Person"]; + + bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}}; + + bson::BsonDocument dog_document2{{"name", "bob"}, {"breed", "french bulldog"}}; + + auto dog3_object_id = ObjectId::gen(); + bson::BsonDocument dog_document3{ + {"_id", dog3_object_id}, + {"name", "petunia"}, + {"breed", "french bulldog"}, + }; + + auto cat_id_string = random_string(10); + bson::BsonDocument cat_document{ + {"_id", cat_id_string}, + {"name", "luna"}, + {"breed", "scottish fold"}, + }; + + bson::BsonDocument person_document{ + {"firstName", "John"}, + {"lastName", "Johnson"}, + {"age", 30}, + }; + + bson::BsonDocument person_document2{ + {"firstName", "Bob"}, + {"lastName", "Johnson"}, + {"age", 30}, + }; + + bson::BsonDocument bad_document{{"bad", "value"}}; + + dog_collection.delete_many(dog_document, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.delete_many(dog_document2, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.delete_many({}, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.delete_many(person_document, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.delete_many(person_document2, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + SECTION("insert") { + bool processed = false; + ObjectId dog_object_id; + ObjectId dog2_object_id; + + dog_collection.insert_one_bson(bad_document, [&](Optional bson, Optional error) { + CHECK(error); + CHECK(!bson); + }); + + dog_collection.insert_one_bson(dog_document3, [&](Optional value, Optional error) { + REQUIRE_FALSE(error); + auto bson = static_cast(*value); + CHECK(static_cast(bson["insertedId"]) == dog3_object_id); + }); + + cat_collection.insert_one_bson(cat_document, [&](Optional value, Optional error) { + REQUIRE_FALSE(error); + auto bson = static_cast(*value); + CHECK(static_cast(bson["insertedId"]) == cat_id_string); + }); + + dog_collection.delete_many({}, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + cat_collection.delete_one(cat_document, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.insert_one(bad_document, [&](Optional object_id, Optional error) { + CHECK(error); + CHECK(!object_id); + }); + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog_object_id = static_cast(*object_id); + }); + + dog_collection.insert_one(dog_document2, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog2_object_id = static_cast(*object_id); + }); + + dog_collection.insert_one(dog_document3, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK(object_id->type() == bson::Bson::Type::ObjectId); + CHECK(static_cast(*object_id) == dog3_object_id); + }); + + cat_collection.insert_one(cat_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK(object_id->type() == bson::Bson::Type::String); + CHECK(static_cast(*object_id) == cat_id_string); + }); + + person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id, dog3_object_id}); + person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + }); + + dog_collection.delete_many({}, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + cat_collection.delete_one(cat_document, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + bson::BsonArray documents{ + dog_document, + dog_document2, + dog_document3, + }; + + dog_collection.insert_many_bson(documents, [&](Optional value, Optional error) { + REQUIRE_FALSE(error); + auto bson = static_cast(*value); + auto insertedIds = static_cast(bson["insertedIds"]); + }); + + dog_collection.delete_many({}, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.insert_many(documents, [&](std::vector inserted_docs, Optional error) { + REQUIRE_FALSE(error); + CHECK(inserted_docs.size() == 3); + CHECK(inserted_docs[0].type() == bson::Bson::Type::ObjectId); + CHECK(inserted_docs[1].type() == bson::Bson::Type::ObjectId); + CHECK(inserted_docs[2].type() == bson::Bson::Type::ObjectId); + CHECK(static_cast(inserted_docs[2]) == dog3_object_id); + processed = true; + }); + + CHECK(processed); + } + + SECTION("find") { + bool processed = false; + + dog_collection.find(dog_document, [&](Optional document_array, Optional error) { + REQUIRE_FALSE(error); + CHECK((*document_array).size() == 0); + }); + + dog_collection.find_bson(dog_document, {}, [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK(static_cast(*bson).size() == 0); + }); + + dog_collection.find_one(dog_document, [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + CHECK(!document); + }); + + dog_collection.find_one_bson(dog_document, {}, [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK((!bson || bson::holds_alternative(*bson))); + }); + + ObjectId dog_object_id; + ObjectId dog2_object_id; + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog_object_id = static_cast(*object_id); + }); + + dog_collection.insert_one(dog_document2, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog2_object_id = static_cast(*object_id); + }); + + person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id}); + person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + }); + + dog_collection.find(dog_document, [&](Optional documents, Optional error) { + REQUIRE_FALSE(error); + CHECK((*documents).size() == 1); + }); + + dog_collection.find_bson(dog_document, {}, [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK(static_cast(*bson).size() == 1); + }); + + person_collection.find(person_document, [&](Optional documents, Optional error) { + REQUIRE_FALSE(error); + CHECK((*documents).size() == 1); + }); + + MongoCollection::FindOptions options{ + 2, // document limit + Optional({{"name", 1}, {"breed", 1}}), // project + Optional({{"breed", 1}}) // sort + }; + + dog_collection.find(dog_document, options, + [&](Optional document_array, Optional error) { + REQUIRE_FALSE(error); + CHECK((*document_array).size() == 1); + }); + + dog_collection.find({{"name", "fido"}}, options, + [&](Optional document_array, Optional error) { + REQUIRE_FALSE(error); + CHECK((*document_array).size() == 1); + auto king_charles = static_cast((*document_array)[0]); + CHECK(king_charles["breed"] == "king charles"); + }); + + dog_collection.find_one(dog_document, [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto name = (*document)["name"]; + CHECK(name == "fido"); + }); + + dog_collection.find_one(dog_document, options, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto name = (*document)["name"]; + CHECK(name == "fido"); + }); + + dog_collection.find_one_bson(dog_document, options, [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + auto name = (static_cast(*bson))["name"]; + CHECK(name == "fido"); + }); + + dog_collection.find(dog_document, [&](Optional documents, Optional error) { + REQUIRE_FALSE(error); + CHECK((*documents).size() == 1); + }); + + dog_collection.find_one_and_delete(dog_document, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + REQUIRE(document); + }); + + dog_collection.find_one_and_delete({{}}, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + REQUIRE(document); + }); + + dog_collection.find_one_and_delete({{"invalid", "key"}}, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + CHECK(!document); + }); + + dog_collection.find_one_and_delete_bson({{"invalid", "key"}}, {}, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK((!bson || bson::holds_alternative(*bson))); + }); + + dog_collection.find(dog_document, [&](Optional documents, Optional error) { + REQUIRE_FALSE(error); + CHECK((*documents).size() == 0); + processed = true; + }); + + CHECK(processed); + } + + SECTION("count and aggregate") { + bool processed = false; + + ObjectId dog_object_id; + ObjectId dog2_object_id; + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + }); + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog_object_id = static_cast(*object_id); + }); + + dog_collection.insert_one(dog_document2, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog2_object_id = static_cast(*object_id); + }); + + person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id}); + person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + }); + + bson::BsonDocument match{{"$match", bson::BsonDocument({{"name", "fido"}})}}; + + bson::BsonDocument group{{"$group", bson::BsonDocument({{"_id", "$name"}})}}; + + bson::BsonArray pipeline{match, group}; + + dog_collection.aggregate(pipeline, [&](Optional documents, Optional error) { + REQUIRE_FALSE(error); + CHECK((*documents).size() == 1); + }); + + dog_collection.aggregate_bson(pipeline, [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK(static_cast(*bson).size() == 1); + }); + + dog_collection.count({{"breed", "king charles"}}, [&](uint64_t count, Optional error) { + REQUIRE_FALSE(error); + CHECK(count == 2); + }); + + dog_collection.count_bson({{"breed", "king charles"}}, 0, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK(static_cast(*bson) == 2); + }); + + dog_collection.count({{"breed", "french bulldog"}}, [&](uint64_t count, Optional error) { + REQUIRE_FALSE(error); + CHECK(count == 1); + }); + + dog_collection.count({{"breed", "king charles"}}, 1, [&](uint64_t count, Optional error) { + REQUIRE_FALSE(error); + CHECK(count == 1); + }); + + person_collection.count( + {{"firstName", "John"}, {"lastName", "Johnson"}, {"age", bson::BsonDocument({{"$gt", 25}})}}, 1, + [&](uint64_t count, Optional error) { + REQUIRE_FALSE(error); + CHECK(count == 1); + processed = true; + }); + + CHECK(processed); + } + + SECTION("find and update") { + bool processed = false; + + MongoCollection::FindOneAndModifyOptions find_and_modify_options{ + Optional({{"name", 1}, {"breed", 1}}), // project + Optional({{"name", 1}}), // sort, + true, // upsert + true // return new doc + }; + + dog_collection.find_one_and_update(dog_document, dog_document2, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + CHECK(!document); + }); + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + }); + + dog_collection.find_one_and_update(dog_document, dog_document2, find_and_modify_options, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto breed = static_cast((*document)["breed"]); + CHECK(breed == "french bulldog"); + }); + + dog_collection.find_one_and_update(dog_document2, dog_document, find_and_modify_options, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto breed = static_cast((*document)["breed"]); + CHECK(breed == "king charles"); + }); + + dog_collection.find_one_and_update_bson(dog_document, dog_document2, find_and_modify_options, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + auto breed = static_cast( + static_cast(*bson)["breed"]); + CHECK(breed == "french bulldog"); + }); + + dog_collection.find_one_and_update_bson(dog_document2, dog_document, find_and_modify_options, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + auto breed = static_cast( + static_cast(*bson)["breed"]); + CHECK(breed == "king charles"); + }); + + dog_collection.find_one_and_update({{"name", "invalid name"}}, {{"name", "some name"}}, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + CHECK(!document); + processed = true; + }); + CHECK(processed); + processed = false; + + dog_collection.find_one_and_update({{"name", "invalid name"}}, {{}}, find_and_modify_options, + [&](Optional document, Optional error) { + REQUIRE(error); + CHECK(error->reason() == "insert not permitted"); + CHECK(!document); + processed = true; + }); + CHECK(processed); + } + + SECTION("update") { + bool processed = false; + ObjectId dog_object_id; + + dog_collection.update_one(dog_document, dog_document2, true, + [&](MongoCollection::UpdateResult result, Optional error) { + REQUIRE_FALSE(error); + CHECK((*result.upserted_id).to_string() != ""); + }); + + dog_collection.update_one(dog_document2, dog_document, + [&](MongoCollection::UpdateResult result, Optional error) { + REQUIRE_FALSE(error); + CHECK(!result.upserted_id); + }); + + cat_collection.update_one({}, cat_document, true, + [&](MongoCollection::UpdateResult result, Optional error) { + REQUIRE_FALSE(error); + CHECK(result.upserted_id->type() == bson::Bson::Type::String); + CHECK(result.upserted_id == cat_id_string); + }); + + dog_collection.delete_many({}, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + cat_collection.delete_many({}, [&](uint64_t, Optional error) { + REQUIRE_FALSE(error); + }); + + dog_collection.update_one_bson(dog_document, dog_document2, true, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + auto upserted_id = static_cast(*bson)["upsertedId"]; + + REQUIRE(upserted_id.type() == bson::Bson::Type::ObjectId); + }); + + dog_collection.update_one_bson(dog_document2, dog_document, true, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + auto document = static_cast(*bson); + auto foundUpsertedId = document.find("upsertedId") != document.end(); + REQUIRE(!foundUpsertedId); + }); + + cat_collection.update_one_bson({}, cat_document, true, + [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + auto upserted_id = static_cast(*bson)["upsertedId"]; + REQUIRE(upserted_id.type() == bson::Bson::Type::String); + REQUIRE(upserted_id == cat_id_string); + }); + + person_document["dogs"] = bson::BsonArray(); + bson::BsonDocument person_document_copy = bson::BsonDocument(person_document); + person_document_copy["dogs"] = bson::BsonArray({dog_object_id}); + person_collection.update_one(person_document, person_document, true, + [&](MongoCollection::UpdateResult, Optional error) { + REQUIRE_FALSE(error); + processed = true; + }); + + CHECK(processed); + } + + SECTION("update many") { + bool processed = false; + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + }); + + dog_collection.update_many(dog_document2, dog_document, true, + [&](MongoCollection::UpdateResult result, Optional error) { + REQUIRE_FALSE(error); + CHECK((*result.upserted_id).to_string() != ""); + }); + + dog_collection.update_many(dog_document2, dog_document, + [&](MongoCollection::UpdateResult result, Optional error) { + REQUIRE_FALSE(error); + CHECK(!result.upserted_id); + processed = true; + }); + + CHECK(processed); + } + + SECTION("find and replace") { + bool processed = false; + ObjectId dog_object_id; + ObjectId person_object_id; + + MongoCollection::FindOneAndModifyOptions find_and_modify_options{ + Optional({{"name", "fido"}}), // project + Optional({{"name", 1}}), // sort, + true, // upsert + true // return new doc + }; + + dog_collection.find_one_and_replace(dog_document, dog_document2, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + CHECK(!document); + }); + + dog_collection.insert_one(dog_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + dog_object_id = static_cast(*object_id); + }); + + dog_collection.find_one_and_replace(dog_document, dog_document2, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto name = static_cast((*document)["name"]); + CHECK(name == "fido"); + }); + + dog_collection.find_one_and_replace(dog_document2, dog_document, find_and_modify_options, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto name = static_cast((*document)["name"]); + CHECK(static_cast(name) == "fido"); + }); + + person_document["dogs"] = bson::BsonArray({dog_object_id}); + person_document2["dogs"] = bson::BsonArray({dog_object_id}); + person_collection.insert_one(person_document, [&](Optional object_id, Optional error) { + REQUIRE_FALSE(error); + CHECK((*object_id).to_string() != ""); + person_object_id = static_cast(*object_id); + }); + + MongoCollection::FindOneAndModifyOptions person_find_and_modify_options{ + Optional({{"firstName", 1}}), // project + Optional({{"firstName", 1}}), // sort, + false, // upsert + true // return new doc + }; + + person_collection.find_one_and_replace(person_document, person_document2, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto name = static_cast((*document)["firstName"]); + // Should return the old document + CHECK(name == "John"); + processed = true; + }); + + person_collection.find_one_and_replace(person_document2, person_document, person_find_and_modify_options, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + auto name = static_cast((*document)["firstName"]); + // Should return new document, Bob -> John + CHECK(name == "John"); + }); + + person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, + [&](Optional document, Optional error) { + // If a document is not found then null will be returned for the + // document and no error will be returned + REQUIRE_FALSE(error); + CHECK(!document); + }); + + person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, person_find_and_modify_options, + [&](Optional document, Optional error) { + REQUIRE_FALSE(error); + CHECK(!document); + processed = true; + }); + + CHECK(processed); + } + + SECTION("delete") { + + bool processed = false; + + bson::BsonArray documents; + documents.assign(3, dog_document); + + dog_collection.insert_many(documents, [&](std::vector inserted_docs, Optional error) { + REQUIRE_FALSE(error); + CHECK(inserted_docs.size() == 3); + }); + + MongoCollection::FindOneAndModifyOptions find_and_modify_options{ + Optional({{"name", "fido"}}), // project + Optional({{"name", 1}}), // sort, + true, // upsert + true // return new doc + }; + + dog_collection.delete_one(dog_document, [&](uint64_t deleted_count, Optional error) { + REQUIRE_FALSE(error); + CHECK(deleted_count >= 1); + }); + + dog_collection.delete_many(dog_document, [&](uint64_t deleted_count, Optional error) { + REQUIRE_FALSE(error); + CHECK(deleted_count >= 1); + processed = true; + }); + + person_collection.delete_many_bson(person_document, [&](Optional bson, Optional error) { + REQUIRE_FALSE(error); + CHECK(static_cast(static_cast(*bson)["deletedCount"]) >= 1); + processed = true; + }); + + CHECK(processed); + } +} + +// MARK: - Push Notifications Tests + +TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") { + TestAppSession session; + auto app = session.app(); + std::shared_ptr sync_user = app->backing_store()->get_current_user(); + + SECTION("register") { + bool processed; + + app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional error) { + REQUIRE_FALSE(error); + processed = true; + }); + + CHECK(processed); + } + /* + // FIXME: It seems this test fails when the two register_device calls are invoked too quickly, + // The error returned will be 'Device not found' on the second register_device call. + SECTION("register twice") { + // registering the same device twice should not result in an error + bool processed; + + app->push_notification_client("gcm").register_device("hello", + sync_user, + [&](Optional error) { + REQUIRE_FALSE(error); + }); + + app->push_notification_client("gcm").register_device("hello", + sync_user, + [&](Optional error) { + REQUIRE_FALSE(error); + processed = true; + }); + + CHECK(processed); + } + */ + SECTION("deregister") { + bool processed; + + app->push_notification_client("gcm").deregister_device(sync_user, [&](Optional error) { + REQUIRE_FALSE(error); + processed = true; + }); + CHECK(processed); + } + + SECTION("register with unavailable service") { + bool processed; + + app->push_notification_client("gcm_blah").register_device("hello", sync_user, [&](Optional error) { + REQUIRE(error); + CHECK(error->reason() == "service not found: 'gcm_blah'"); + processed = true; + }); + CHECK(processed); + } + + SECTION("register with logged out user") { + bool processed; + + app->log_out([=](Optional error) { + REQUIRE_FALSE(error); + }); + + app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional error) { + REQUIRE(error); + processed = true; + }); + + app->push_notification_client("gcm").register_device("hello", nullptr, [&](Optional error) { + REQUIRE(error); + processed = true; + }); + + CHECK(processed); + } +} + +// MARK: - Token refresh + +TEST_CASE("app: token refresh", "[sync][app][token][baas]") { + TestAppSession session; + auto app = session.app(); + std::shared_ptr sync_user = app->backing_store()->get_current_user(); + sync_user->update_access_token(ENCODE_FAKE_JWT("fake_access_token")); + + auto remote_client = app->backing_store()->get_current_user()->mongo_client("BackingDB"); + auto app_session = get_runtime_app_session(); + auto db = remote_client.db(app_session.config.mongo_dbname); + auto dog_collection = db["Dog"]; + bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}}; + + SECTION("access token should refresh") { + /* + Expected sequence of events: + - `find_one` tries to hit the server with a bad access token + - Server returns an error because of the bad token, error should be something like: + {\"error\":\"json: cannot unmarshal array into Go value of type map[string]interface + {}\",\"link\":\"http://localhost:9090/groups/5f84167e776aa0f9dc27081a/apps/5f841686776aa0f9dc270876/logs?co_id=5f844c8c776aa0f9dc273db6\"} + http_status_code = 401 + custom_status_code = 0 + - App::handle_auth_failure is then called and an attempt to refresh the access token will be peformed. + - If the token refresh was successful, the original request will retry and we should expect no error in the + callback of `find_one` + */ + dog_collection.find_one(dog_document, [&](Optional, Optional error) { + REQUIRE_FALSE(error); + }); + } +} + +TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") { + TestAppSession session; + auto app = session.app(); + auto user = app->current_user(); + + SECTION("custom user data happy path") { + bool processed = false; + app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})}, + [&](auto response, auto error) { + CHECK(error == none); + CHECK(response); + CHECK(*response == true); + processed = true; + }); + CHECK(processed); + processed = false; + app->refresh_custom_data(user, [&](auto) { + processed = true; + }); + CHECK(processed); + auto data = *user->custom_data(); + CHECK(data["favorite_color"] == "green"); + } +} + +TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") { + TestAppSession session; + auto app = session.app(); + auto jwt = create_jwt(session.app()->config().app_id); + + SECTION("jwt happy path") { + bool processed = false; + + std::shared_ptr user = log_in(app, AppCredentials::custom(jwt)); + + app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})}, + [&](auto response, auto error) { + CHECK(error == none); + CHECK(response); + CHECK(*response == true); + processed = true; + }); + CHECK(processed); + processed = false; + app->refresh_custom_data(user, [&](auto) { + processed = true; + }); + CHECK(processed); + auto metadata = user->user_profile(); + auto custom_data = *user->custom_data(); + CHECK(custom_data["name"] == "Not Foo Bar"); + CHECK(metadata["name"] == "Foo Bar"); + } +} + + +#endif // REALM_ENABLE_AUTH_TESTS + +static OfflineAppSession::Config +offline_unit_test_config(std::shared_ptr transport = instance_of) +{ + return OfflineAppSession::Config(transport); +} + +TEST_CASE("app: custom error handling", "[sync][app][custom errors]") { + class CustomErrorTransport : public GenericNetworkTransport { + public: + CustomErrorTransport(int code, const std::string& message) + : m_code(code) + , m_message(message) + { + } + + void send_request_to_server(const Request&, util::UniqueFunction&& completion) override + { + completion(Response{0, m_code, HttpHeaders(), m_message}); + } + + private: + int m_code; + std::string m_message; + }; + + SECTION("custom code and message is sent back") { + OfflineAppSession session(offline_unit_test_config(std::make_shared(1001, "Boom!"))); + auto error = failed_log_in(session.app()); + CHECK(error.is_custom_error()); + CHECK(*error.additional_status_code == 1001); + CHECK(error.reason() == "Boom!"); + } +} + +// MARK: - Unit Tests + +TEST_CASE("subscribable unit tests", "[sync][app]") { + struct Foo : public Subscribable { + void event() + { + emit_change_to_subscribers(*this); + } + }; + + auto foo = Foo(); + + SECTION("subscriber receives events") { + auto event_count = 0; + auto token = foo.subscribe([&event_count](auto&) { + event_count++; + }); + + foo.event(); + foo.event(); + foo.event(); + + CHECK(event_count == 3); + } + + SECTION("subscriber can unsubscribe") { + auto event_count = 0; + auto token = foo.subscribe([&event_count](auto&) { + event_count++; + }); + + foo.event(); + CHECK(event_count == 1); + + foo.unsubscribe(token); + foo.event(); + CHECK(event_count == 1); + } + + SECTION("subscriber is unsubscribed on dtor") { + auto event_count = 0; + { + auto token = foo.subscribe([&event_count](auto&) { + event_count++; + }); + + foo.event(); + CHECK(event_count == 1); + } + foo.event(); + CHECK(event_count == 1); + } + + SECTION("multiple subscribers receive events") { + auto event_count = 0; + { + auto token1 = foo.subscribe([&event_count](auto&) { + event_count++; + }); + auto token2 = foo.subscribe([&event_count](auto&) { + event_count++; + }); + + foo.event(); + CHECK(event_count == 2); + } + foo.event(); + CHECK(event_count == 2); + } +} + +TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { + auto config = offline_unit_test_config(); + static_cast(config.transport.get())->set_profile(profile_0); + + SECTION("login_anonymous good") { + std::string shared_storage_path = util::make_temp_dir(); + UnitTestTransport::access_token = good_access_token; + config.storage_path = shared_storage_path; + config.delete_storage = false; + { + OfflineAppSession tas(std::move(config)); + auto app = tas.app(); + auto user = log_in(app); + + REQUIRE(user->identities().size() == 1); + CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); + SyncUserProfile user_profile = user->user_profile(); + + CHECK(user_profile.name() == profile_0_name); + CHECK(user_profile.first_name() == profile_0_first_name); + CHECK(user_profile.last_name() == profile_0_last_name); + CHECK(user_profile.email() == profile_0_email); + CHECK(user_profile.picture_url() == profile_0_picture_url); + CHECK(user_profile.gender() == profile_0_gender); + CHECK(user_profile.birthday() == profile_0_birthday); + CHECK(user_profile.min_age() == profile_0_min_age); + CHECK(user_profile.max_age() == profile_0_max_age); + } + App::clear_cached_apps(); + // assert everything is stored properly between runs + { + auto config2 = offline_unit_test_config(); + config2.storage_path = shared_storage_path; + config2.delete_storage = true; + OfflineAppSession tas(std::move(config2)); + + auto app = tas.app(); + REQUIRE(app->all_users().size() == 1); + auto user = app->all_users()[0]; + REQUIRE(user->identities().size() == 1); + CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); + SyncUserProfile user_profile = user->user_profile(); + + CHECK(user_profile.name() == profile_0_name); + CHECK(user_profile.first_name() == profile_0_first_name); + CHECK(user_profile.last_name() == profile_0_last_name); + CHECK(user_profile.email() == profile_0_email); + CHECK(user_profile.picture_url() == profile_0_picture_url); + CHECK(user_profile.gender() == profile_0_gender); + CHECK(user_profile.birthday() == profile_0_birthday); + CHECK(user_profile.min_age() == profile_0_min_age); + CHECK(user_profile.max_age() == profile_0_max_age); + } + } + + SECTION("login_anonymous bad") { + struct transport : UnitTestTransport { + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/login") != std::string::npos) { + completion({200, 0, {}, user_json(bad_access_token).dump()}); + } + else { + UnitTestTransport::send_request_to_server(request, std::move(completion)); + } + } + }; + + config.transport = instance_of; + OfflineAppSession tas(std::move(config)); + auto error = failed_log_in(tas.app()); + CHECK(error.reason() == std::string("jwt missing parts")); + CHECK(error.code_string() == "BadToken"); + CHECK(error.is_json_error()); + CHECK(error.code() == ErrorCodes::BadToken); + } + + SECTION("login_anonynous multiple users") { + UnitTestTransport::access_token = good_access_token; + OfflineAppSession tas(std::move(config)); + auto app = tas.app(); + + auto user1 = log_in(app); + auto user2 = log_in(app, AppCredentials::anonymous(false)); + CHECK(user1 != user2); + } +} + +TEST_CASE("app: UserAPIKeyProviderClient unit_tests", "[sync][app][user][api key]") { + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + auto client = app->provider_client(); + + std::shared_ptr logged_in_user = + app->backing_store()->get_user("userid", good_access_token, good_access_token, dummy_device_id); + bool processed = false; + ObjectId obj_id(UnitTestTransport::api_key_id.c_str()); + + SECTION("create api key") { + client.create_api_key(UnitTestTransport::api_key_name, logged_in_user, + [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.disabled == false); + CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id); + CHECK(user_api_key.key == UnitTestTransport::api_key); + CHECK(user_api_key.name == UnitTestTransport::api_key_name); + }); + } + + SECTION("fetch api key") { + client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_key.disabled == false); + CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id); + CHECK(user_api_key.name == UnitTestTransport::api_key_name); + }); + } + + SECTION("fetch api keys") { + client.fetch_api_keys(logged_in_user, + [&](std::vector user_api_keys, Optional error) { + REQUIRE_FALSE(error); + CHECK(user_api_keys.size() == 2); + for (auto user_api_key : user_api_keys) { + CHECK(user_api_key.disabled == false); + CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id); + CHECK(user_api_key.name == UnitTestTransport::api_key_name); + } + processed = true; + }); + CHECK(processed); + } +} + + +TEST_CASE("app: user_semantics", "[sync][app][user]") { + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + + const auto login_user_email_pass = [=] { + return log_in(app, AppCredentials::username_password("bob", "thompson")); + }; + const auto login_user_anonymous = [=] { + return log_in(app, AppCredentials::anonymous()); + }; + + CHECK(!app->current_user()); + + int event_processed = 0; + auto token = app->subscribe([&event_processed](auto&) { + event_processed++; + }); + + SECTION("current user is populated") { + const auto user1 = login_user_anonymous(); + CHECK(app->current_user()->identity() == user1->identity()); + CHECK(event_processed == 1); + } + + SECTION("current user is updated on login") { + const auto user1 = login_user_anonymous(); + CHECK(app->current_user()->identity() == user1->identity()); + const auto user2 = login_user_email_pass(); + CHECK(app->current_user()->identity() == user2->identity()); + CHECK(user1->identity() != user2->identity()); + CHECK(event_processed == 2); + } + + SECTION("current user is updated to last used user on logout") { + const auto user1 = login_user_anonymous(); + CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + + const auto user2 = login_user_email_pass(); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn); + CHECK(app->current_user()->identity() == user2->identity()); + CHECK(user1 != user2); + + // should reuse existing session + const auto user3 = login_user_anonymous(); + CHECK(user3 == user1); + + auto user_events_processed = 0; + auto _ = user3->subscribe([&user_events_processed](auto&) { + user_events_processed++; + }); + + app->log_out([](auto) {}); + CHECK(user_events_processed == 1); + + CHECK(app->current_user()->identity() == user2->identity()); + + CHECK(app->all_users().size() == 1); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + + CHECK(event_processed == 4); + } + + SECTION("anon users are removed on logout") { + const auto user1 = login_user_anonymous(); + CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + + const auto user2 = login_user_anonymous(); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + CHECK(app->all_users().size() == 1); + CHECK(app->current_user()->identity() == user2->identity()); + CHECK(user1->identity() == user2->identity()); + + app->log_out([](auto) {}); + CHECK(app->all_users().size() == 0); + + CHECK(event_processed == 3); + } + + SECTION("logout user") { + auto user1 = login_user_email_pass(); + auto user2 = login_user_anonymous(); + + // Anonymous users are special + app->log_out(user2, [](Optional error) { + REQUIRE_FALSE(error); + }); + CHECK(user2->state() == SyncUser::State::Removed); + + // Other users can be LoggedOut + app->log_out(user1, [](Optional error) { + REQUIRE_FALSE(error); + }); + CHECK(user1->state() == SyncUser::State::LoggedOut); + + // Logging out already logged out users, does nothing + app->log_out(user1, [](Optional error) { + REQUIRE_FALSE(error); + }); + CHECK(user1->state() == SyncUser::State::LoggedOut); + + app->log_out(user2, [](Optional error) { + REQUIRE_FALSE(error); + }); + CHECK(user2->state() == SyncUser::State::Removed); + + CHECK(event_processed == 4); + } + + SECTION("unsubscribed observers no longer process events") { + app->unsubscribe(token); + + const auto user1 = login_user_anonymous(); + CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + + const auto user2 = login_user_anonymous(); + CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); + CHECK(app->all_users().size() == 1); + CHECK(app->current_user()->identity() == user2->identity()); + CHECK(user1->identity() == user2->identity()); + + app->log_out([](auto) {}); + CHECK(app->all_users().size() == 0); + + CHECK(event_processed == 0); + } +} + +struct ErrorCheckingTransport : public GenericNetworkTransport { + ErrorCheckingTransport(Response* r) + : m_response(r) + { + } + void send_request_to_server(const Request&, util::UniqueFunction&& completion) override + { + completion(Response(*m_response)); + } + +private: + Response* m_response; +}; + +TEST_CASE("app: response error handling", "[sync][app]") { + std::string response_body = nlohmann::json({{"access_token", good_access_token}, + {"refresh_token", good_access_token}, + {"user_id", "Brown Bear"}, + {"device_id", "Panda Bear"}}) + .dump(); + + Response response{200, 0, {{"Content-Type", "text/plain"}}, response_body}; + OfflineAppSession tas(offline_unit_test_config(std::make_shared(&response))); + + auto app = tas.app(); + + SECTION("http 404") { + response.http_status_code = 404; + auto error = failed_log_in(app); + CHECK(!error.is_json_error()); + CHECK(!error.is_custom_error()); + CHECK(!error.is_service_error()); + CHECK(error.is_http_error()); + CHECK(*error.additional_status_code == 404); + CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos); + } + SECTION("http 500") { + response.http_status_code = 500; + auto error = failed_log_in(app); + CHECK(!error.is_json_error()); + CHECK(!error.is_custom_error()); + CHECK(!error.is_service_error()); + CHECK(error.is_http_error()); + CHECK(*error.additional_status_code == 500); + CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos); + CHECK(error.link_to_server_logs.empty()); + } + + SECTION("custom error code") { + response.custom_status_code = 42; + response.body = "Custom error message"; + auto error = failed_log_in(app); + CHECK(!error.is_http_error()); + CHECK(!error.is_json_error()); + CHECK(!error.is_service_error()); + CHECK(error.is_custom_error()); + CHECK(*error.additional_status_code == 42); + CHECK(error.reason() == std::string("Custom error message")); + CHECK(error.link_to_server_logs.empty()); + } + + SECTION("session error code") { + response.headers = HttpHeaders{{"Content-Type", "application/json"}}; + response.http_status_code = 400; + response.body = nlohmann::json({{"error_code", "MongoDBError"}, + {"error", "a fake MongoDB error message!"}, + {"access_token", good_access_token}, + {"refresh_token", good_access_token}, + {"user_id", "Brown Bear"}, + {"device_id", "Panda Bear"}, + {"link", "http://...whatever the server passes us"}}) + .dump(); + auto error = failed_log_in(app); + CHECK(!error.is_http_error()); + CHECK(!error.is_json_error()); + CHECK(!error.is_custom_error()); + CHECK(error.is_service_error()); + CHECK(error.code() == ErrorCodes::MongoDBError); + CHECK(error.reason() == std::string("a fake MongoDB error message!")); + CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us")); + } + + SECTION("json error code") { + response.body = "this: is not{} a valid json body!"; + auto error = failed_log_in(app); + CHECK(!error.is_http_error()); + CHECK(error.is_json_error()); + CHECK(!error.is_custom_error()); + CHECK(!error.is_service_error()); + CHECK(error.code() == ErrorCodes::MalformedJson); + CHECK(error.reason() == + std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error " + "while parsing value - invalid literal; last read: 'th'")); + CHECK(error.code_string() == "MalformedJson"); + } +} + +TEST_CASE("app: switch user", "[sync][app][user]") { + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + + bool processed = false; + + SECTION("switch user expect success") { + CHECK(app->backing_store()->all_users().size() == 0); + + // Log in user 1 + auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password")); + CHECK(app->backing_store()->get_current_user() == user_a); + + // Log in user 2 + auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password")); + CHECK(app->backing_store()->get_current_user() == user_b); + + CHECK(app->backing_store()->all_users().size() == 2); + + auto user1 = app->switch_user(user_a); + CHECK(user1 == user_a); + + CHECK(app->backing_store()->get_current_user() == user_a); + + auto user2 = app->switch_user(user_b); + CHECK(user2 == user_b); + + CHECK(app->backing_store()->get_current_user() == user_b); + processed = true; + CHECK(processed); + } + + SECTION("cannot switch to a logged out but not removed user") { + CHECK(app->backing_store()->all_users().size() == 0); + + // Log in user 1 + auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password")); + CHECK(app->backing_store()->get_current_user() == user_a); + + app->log_out([&](Optional error) { + REQUIRE_FALSE(error); + }); + + CHECK(app->backing_store()->get_current_user() == nullptr); + CHECK(user_a->state() == SyncUser::State::LoggedOut); + + // Log in user 2 + auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password")); + CHECK(app->backing_store()->get_current_user() == user_b); + CHECK(app->backing_store()->all_users().size() == 2); + + REQUIRE_THROWS_AS(app->switch_user(user_a), AppError); + CHECK(app->backing_store()->get_current_user() == user_b); + } +} + +TEST_CASE("app: remove anonymous user", "[sync][app][user]") { + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + + SECTION("remove user expect success") { + CHECK(app->backing_store()->all_users().size() == 0); + + // Log in user 1 + auto user_a = log_in(app); + CHECK(user_a->state() == SyncUser::State::LoggedIn); + + app->log_out(user_a, [&](Optional error) { + REQUIRE_FALSE(error); + // a logged out anon user will be marked as Removed, not LoggedOut + CHECK(user_a->state() == SyncUser::State::Removed); + }); + CHECK(app->backing_store()->all_users().empty()); + + app->remove_user(user_a, [&](Optional error) { + CHECK(error->reason() == "User has already been removed"); + CHECK(app->backing_store()->all_users().size() == 0); + }); + + // Log in user 2 + auto user_b = log_in(app); + CHECK(app->backing_store()->get_current_user() == user_b); + CHECK(user_b->state() == SyncUser::State::LoggedIn); + CHECK(app->backing_store()->all_users().size() == 1); + + app->remove_user(user_b, [&](Optional error) { + REQUIRE_FALSE(error); + CHECK(app->backing_store()->all_users().size() == 0); + }); + + CHECK(app->backing_store()->get_current_user() == nullptr); + + // check both handles are no longer valid + CHECK(user_a->state() == SyncUser::State::Removed); + CHECK(user_b->state() == SyncUser::State::Removed); + } +} + +TEST_CASE("app: remove user with credentials", "[sync][app][user]") { + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + + SECTION("log in, log out and remove") { + CHECK(app->backing_store()->all_users().size() == 0); + CHECK(app->backing_store()->get_current_user() == nullptr); + + auto user = log_in(app, AppCredentials::username_password("email", "pass")); + + CHECK(user->state() == SyncUser::State::LoggedIn); + + app->log_out(user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + + CHECK(user->state() == SyncUser::State::LoggedOut); + + app->remove_user(user, [&](Optional error) { + REQUIRE_FALSE(error); + }); + CHECK(app->backing_store()->all_users().size() == 0); + + Optional error; + app->remove_user(user, [&](Optional err) { + error = err; + }); + CHECK(error->code() > 0); + CHECK(app->backing_store()->all_users().size() == 0); + CHECK(user->state() == SyncUser::State::Removed); + } +} + +TEST_CASE("app: link_user", "[sync][app][user]") { + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + + auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); + auto password = random_string(10); + + auto custom_credentials = AppCredentials::facebook("a_token"); + auto email_pass_credentials = AppCredentials::username_password(email, password); + + auto sync_user = log_in(app, email_pass_credentials); + REQUIRE(sync_user->identities().size() == 2); + CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword); + + SECTION("successful link") { + bool processed = false; + app->link_user(sync_user, custom_credentials, [&](std::shared_ptr user, Optional error) { + REQUIRE_FALSE(error); + REQUIRE(user); + CHECK(user->identity() == sync_user->identity()); + processed = true; + }); + CHECK(processed); + } + + SECTION("link_user should fail when logged out") { + app->log_out([&](Optional error) { + REQUIRE_FALSE(error); + }); + + bool processed = false; + app->link_user(sync_user, custom_credentials, [&](std::shared_ptr user, Optional error) { + CHECK(error->reason() == "The specified user is not logged in."); + CHECK(!user); + processed = true; + }); + CHECK(processed); + } +} + +TEST_CASE("app: auth providers", "[sync][app][user]") { + SECTION("auth providers facebook") { + auto credentials = AppCredentials::facebook("a_token"); + CHECK(credentials.provider() == AuthProvider::FACEBOOK); + CHECK(credentials.provider_as_string() == IdentityProviderFacebook); + CHECK(credentials.serialize_as_bson() == + bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}}); + } + + SECTION("auth providers anonymous") { + auto credentials = AppCredentials::anonymous(); + CHECK(credentials.provider() == AuthProvider::ANONYMOUS); + CHECK(credentials.provider_as_string() == IdentityProviderAnonymous); + CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}}); + } + + SECTION("auth providers anonymous no reuse") { + auto credentials = AppCredentials::anonymous(false); + CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE); + CHECK(credentials.provider_as_string() == IdentityProviderAnonymous); + CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}}); + } + + SECTION("auth providers google authCode") { + auto credentials = AppCredentials::google(AuthCode("a_token")); + CHECK(credentials.provider() == AuthProvider::GOOGLE); + CHECK(credentials.provider_as_string() == IdentityProviderGoogle); + CHECK(credentials.serialize_as_bson() == + bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}}); + } + + SECTION("auth providers google idToken") { + auto credentials = AppCredentials::google(IdToken("a_token")); + CHECK(credentials.provider() == AuthProvider::GOOGLE); + CHECK(credentials.provider_as_string() == IdentityProviderGoogle); + CHECK(credentials.serialize_as_bson() == + bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}}); + } + + SECTION("auth providers apple") { + auto credentials = AppCredentials::apple("a_token"); + CHECK(credentials.provider() == AuthProvider::APPLE); + CHECK(credentials.provider_as_string() == IdentityProviderApple); + CHECK(credentials.serialize_as_bson() == + bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}}); + } + + SECTION("auth providers custom") { + auto credentials = AppCredentials::custom("a_token"); + CHECK(credentials.provider() == AuthProvider::CUSTOM); + CHECK(credentials.provider_as_string() == IdentityProviderCustom); + CHECK(credentials.serialize_as_bson() == + bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}}); + } + + SECTION("auth providers username password") { + auto credentials = AppCredentials::username_password("user", "pass"); + CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD); + CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword); + CHECK(credentials.serialize_as_bson() == + bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}}); + } + + SECTION("auth providers function") { + bson::BsonDocument function_params{{"name", "mongo"}}; + auto credentials = AppCredentials::function(function_params); + CHECK(credentials.provider() == AuthProvider::FUNCTION); + CHECK(credentials.provider_as_string() == IdentityProviderFunction); + CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}}); + } + + SECTION("auth providers api key") { + auto credentials = AppCredentials::api_key("a key"); + CHECK(credentials.provider() == AuthProvider::API_KEY); + CHECK(credentials.provider_as_string() == IdentityProviderAPIKey); + CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}}); + CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY); + } +} + +TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") { + auto setup_user = [](std::shared_ptr app) { + if (app->backing_store()->get_current_user()) { + return; + } + app->backing_store()->get_user("a_user_id", good_access_token, good_access_token, dummy_device_id); + }; + + SECTION("refresh custom data happy path") { + static bool session_route_hit = false; + + struct transport : UnitTestTransport { + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/session") != std::string::npos) { + session_route_hit = true; + nlohmann::json json{{"access_token", good_access_token}}; + completion({200, 0, {}, json.dump()}); + } + else { + UnitTestTransport::send_request_to_server(request, std::move(completion)); + } + } + }; + OfflineAppSession tas(offline_unit_test_config(instance_of)); + auto app = tas.app(); + setup_user(app); + + bool processed = false; + app->refresh_custom_data(app->backing_store()->get_current_user(), [&](const Optional& error) { + REQUIRE_FALSE(error); + CHECK(session_route_hit); + processed = true; + }); + CHECK(processed); + } + + SECTION("refresh custom data sad path") { + static bool session_route_hit = false; + + struct transport : UnitTestTransport { + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/session") != std::string::npos) { + session_route_hit = true; + nlohmann::json json{{"access_token", bad_access_token}}; + completion({200, 0, {}, json.dump()}); + } + else { + UnitTestTransport::send_request_to_server(request, std::move(completion)); + } + } + }; + OfflineAppSession tas(offline_unit_test_config(instance_of)); + auto app = tas.app(); + setup_user(app); + + bool processed = false; + app->refresh_custom_data(app->backing_store()->get_current_user(), [&](const Optional& error) { + CHECK(error->reason() == "jwt missing parts"); + CHECK(error->code() == ErrorCodes::BadToken); + CHECK(session_route_hit); + processed = true; + }); + CHECK(processed); + } + + SECTION("refresh token ensure flow is correct") { + /* + Expected flow: + Login - this gets access and refresh tokens + Get profile - throw back a 401 error + Refresh token - get a new token for the user + Get profile - get the profile with the new token + */ + + struct transport : GenericNetworkTransport { + bool login_hit = false; + bool get_profile_1_hit = false; + bool get_profile_2_hit = false; + bool refresh_hit = false; + + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/login") != std::string::npos) { + login_hit = true; + completion({200, 0, {}, user_json(good_access_token).dump()}); + } + else if (request.url.find("/profile") != std::string::npos) { + CHECK(login_hit); + + auto item = AppUtils::find_header("Authorization", request.headers); + CHECK(item); + auto access_token = item->second; + // simulated bad token request + if (access_token.find(good_access_token2) != std::string::npos) { + CHECK(login_hit); + CHECK(get_profile_1_hit); + CHECK(refresh_hit); + + get_profile_2_hit = true; + + completion({200, 0, {}, user_profile_json().dump()}); + } + else if (access_token.find(good_access_token) != std::string::npos) { + CHECK(!get_profile_2_hit); + get_profile_1_hit = true; + + completion({401, 0, {}}); + } + } + else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { + CHECK(login_hit); + CHECK(get_profile_1_hit); + CHECK(!get_profile_2_hit); + refresh_hit = true; + + nlohmann::json json{{"access_token", good_access_token2}}; + completion({200, 0, {}, json.dump()}); + } + else if (request.url.find("/location") != std::string::npos) { + CHECK(request.method == HttpMethod::get); + completion({200, + 0, + {}, + "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" + "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}); + } + } + }; + OfflineAppSession tas(offline_unit_test_config(instance_of)); + auto app = tas.app(); + setup_user(app); + REQUIRE(log_in(app)); + } +} + +TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { + constexpr uint64_t timeout_ms = 60000; // this is the default + UnitTestTransport::access_token = good_access_token; + OfflineAppSession tas(offline_unit_test_config()); + auto app = tas.app(); + + std::shared_ptr user = log_in(app); + + using Headers = decltype(Request().headers); + + const auto url_prefix = "field/api/client/v2.0/app/app_id/functions/call?baas_request="sv; + const auto get_request_args = [&](const Request& req) { + REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix); + auto args = req.url.substr(url_prefix.size()); + if (auto amp = args.find('&'); amp != std::string::npos) { + args.resize(amp); + } + + auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args)); + REQUIRE(!!vec); + auto parsed = bson::parse({vec->data(), vec->size()}); + REQUIRE(parsed.type() == bson::Bson::Type::Document); + auto out = parsed.operator const bson::BsonDocument&(); + CHECK(out.size() == 3); + return out; + }; + + const auto make_request = [&](std::shared_ptr user, auto&&... args) { + auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"}); + CHECK(req.method == HttpMethod::get); + CHECK(req.body == ""); + CHECK(req.headers == Headers{{"Accept", "text/event-stream"}}); + CHECK(req.timeout_ms == timeout_ms); + CHECK(req.uses_refresh_token == false); + + auto req_args = get_request_args(req); + CHECK(req_args["name"] == "func"); + CHECK(req_args["service"] == "svc"); + CHECK(req_args["arguments"] == bson::BsonArray{args...}); + + return req; + }; + + SECTION("no args") { + auto req = make_request(nullptr); + CHECK(req.url.find('&') == std::string::npos); + } + SECTION("args") { + auto req = make_request(nullptr, "arg1", "arg2"); + CHECK(req.url.find('&') == std::string::npos); + } + SECTION("percent encoding") { + // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded. + auto req = make_request(nullptr, ">>>>>?????"); + + CHECK(req.url.find('&') == std::string::npos); + CHECK(req.url.find("%2B") != std::string::npos); // + (from >) + CHECK(req.url.find("%2F") != std::string::npos); // / (from ?) + CHECK(req.url.find("%3D") != std::string::npos); // = (tail padding) + CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding) + } + SECTION("with user") { + auto req = make_request(user, "arg1", "arg2"); + + auto amp = req.url.find('&'); + REQUIRE(amp != std::string::npos); + auto tail = req.url.substr(amp); + REQUIRE(tail == ("&baas_at=" + user->access_token())); + } +} + +TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") { + SECTION("with empty map") { + auto profile = SyncUserProfile(bson::BsonDocument()); + CHECK(profile.name() == util::none); + CHECK(profile.email() == util::none); + CHECK(profile.picture_url() == util::none); + CHECK(profile.first_name() == util::none); + CHECK(profile.last_name() == util::none); + CHECK(profile.gender() == util::none); + CHECK(profile.birthday() == util::none); + CHECK(profile.min_age() == util::none); + CHECK(profile.max_age() == util::none); + } + SECTION("with full map") { + auto profile = SyncUserProfile(bson::BsonDocument({ + {"first_name", "Jan"}, + {"last_name", "Jaanson"}, + {"name", "Jan Jaanson"}, + {"email", "jan.jaanson@jaanson.com"}, + {"gender", "none"}, + {"birthday", "January 1, 1970"}, + {"min_age", "0"}, + {"max_age", "100"}, + {"picture_url", "some"}, + })); + CHECK(profile.name() == "Jan Jaanson"); + CHECK(profile.email() == "jan.jaanson@jaanson.com"); + CHECK(profile.picture_url() == "some"); + CHECK(profile.first_name() == "Jan"); + CHECK(profile.last_name() == "Jaanson"); + CHECK(profile.gender() == "none"); + CHECK(profile.birthday() == "January 1, 1970"); + CHECK(profile.min_age() == "0"); + CHECK(profile.max_age() == "100"); + } +} + +TEST_CASE("app: shared instances", "[sync][app]") { + App::Config base_config; + set_app_config_defaults(base_config, instance_of); + + app::BackingStoreConfig bsc; + bsc.metadata_mode = app::BackingStoreConfig::MetadataMode::NoMetadata; + bsc.base_file_path = util::make_temp_dir(); + util::try_make_dir(bsc.base_file_path); + auto cleanup = util::make_scope_exit([&]() noexcept { + realm::util::try_remove_dir_recursive(bsc.base_file_path); + }); + + auto config1 = base_config; + config1.app_id = "app1"; + + auto config2 = base_config; + config2.app_id = "app1"; + config2.base_url = "https://realm.mongodb.com"; // equivalent to default_base_url + + auto config3 = base_config; + config3.app_id = "app2"; + + auto config4 = base_config; + config4.app_id = "app2"; + config4.base_url = "http://localhost:9090"; + + // should all point to same underlying app + auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1, bsc); + auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1, bsc); + auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url); + auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2, bsc); + auto app1_5 = App::get_cached_app(config1.app_id); + + CHECK(app1_1 == app1_2); + CHECK(app1_1 == app1_3); + CHECK(app1_1 == app1_4); + CHECK(app1_1 == app1_5); + + // config3 and config4 should point to different apps + auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3, bsc); + auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url); + auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4, bsc); + auto app2_4 = App::get_cached_app(config3.app_id); + auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url"); + + CHECK(app2_1 == app2_2); + CHECK(app2_1 != app2_3); + CHECK(app2_4 != nullptr); + CHECK(app2_5 == nullptr); + + CHECK(app1_1 != app2_1); + CHECK(app1_1 != app2_3); + CHECK(app1_1 != app2_4); +} diff --git a/test/object-store/sync/app_services.cpp b/test/object-store/sync/app_services.cpp deleted file mode 100644 index 1aadbd1ff03..00000000000 --- a/test/object-store/sync/app_services.cpp +++ /dev/null @@ -1,117 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2023 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include "util/sync/sync_test_utils.hpp" -#include "util/unit_test_transport.hpp" -#include "util/test_file.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace realm; -using namespace realm::app; - -namespace { -std::shared_ptr log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) -{ - if (auto transport = dynamic_cast(app->config().transport.get())) { - transport->set_provider_type(credentials.provider_as_string()); - } - std::shared_ptr user; - app->log_in_with_credentials(credentials, - [&](std::shared_ptr user_arg, util::Optional error) { - REQUIRE_FALSE(error); - REQUIRE(user_arg); - user = std::move(user_arg); - }); - REQUIRE(user); - return user; -} - -AppError failed_log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) -{ - util::Optional err; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user, util::Optional error) { - REQUIRE(error); - REQUIRE_FALSE(user); - err = error; - }); - REQUIRE(err); - return *err; -} - -} // namespace - -namespace realm { -class TestHelper { -public: - static DBRef get_db(Realm& realm) - { - return Realm::Internal::get_db(realm); - } -}; -} // namespace realm - -#if !REALM_ENABLE_AUTH_TESTS || !defined(REALM_MONGODB_ENDPOINT) -static_assert(false, "These tests require a MongoDB instance") -#endif - - TEST_CASE("app services: log in integration", "[app][user][baas][services]") -{ - SECTION("login") { - TestAppSession session; - auto app = session.app(); - app->log_out([](auto) {}); - - int subscribe_processed = 0; - auto token = app->subscribe([&subscribe_processed](auto& app) { - if (!subscribe_processed) { - REQUIRE(app.current_user()); - } - else { - REQUIRE_FALSE(app.current_user()); - } - subscribe_processed++; - }); - - auto user = log_in(app); - CHECK(!user->device_id().empty()); - CHECK(user->has_device_id()); - - bool processed = false; - app->log_out([&](auto error) { - REQUIRE_FALSE(error); - processed = true; - }); - - CHECK(processed); - CHECK(subscribe_processed == 2); - - app->unsubscribe(token); - } -} diff --git a/test/object-store/sync/app_sync_services.cpp b/test/object-store/sync/app_sync_services.cpp new file mode 100644 index 00000000000..0e2845e756b --- /dev/null +++ b/test/object-store/sync/app_sync_services.cpp @@ -0,0 +1,2520 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "collection_fixtures.hpp" +#include "util/sync/baas_admin_api.hpp" +#include "util/sync/sync_test_utils.hpp" +#include "util/unit_test_transport.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace realm; +using namespace realm::app; +using util::any_cast; +using util::Optional; + +using namespace std::string_view_literals; +using namespace std::literals::string_literals; + +#if !REALM_ENABLE_AUTH_TESTS || !defined(REALM_MONGODB_ENDPOINT) +static_assert(false, "These tests require a MongoDB instance") +#endif + + namespace realm +{ + class TestHelper { + public: + static DBRef get_db(Realm& realm) + { + return Realm::Internal::get_db(realm); + } + }; +} // namespace realm + +// MARK: - Sync Tests + +TEST_CASE("app: mixed lists with object links", "[sync][pbs][app][links][baas]") { + const std::string valid_pk_name = "_id"; + + Schema schema{ + {"TopLevel", + { + {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, + {"mixed_array", PropertyType::Mixed | PropertyType::Array | PropertyType::Nullable}, + }}, + {"Target", + { + {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, + {"value", PropertyType::Int}, + }}, + }; + + auto server_app_config = minimal_app_config("set_new_embedded_object", schema); + auto app_session = create_app(server_app_config); + auto partition = random_string(100); + + auto obj_id = ObjectId::gen(); + auto target_id = ObjectId::gen(); + auto mixed_list_values = AnyVector{ + Mixed{int64_t(1234)}, + Mixed{}, + Mixed{target_id}, + }; + { + TestAppSession test_session({app_session, nullptr, DeleteApp{false}}); + SyncTestFile config(test_session.app(), partition, schema); + auto realm = Realm::get_shared_realm(config); + + CppContext c(realm); + realm->begin_transaction(); + auto target_obj = Object::create( + c, realm, "Target", std::any(AnyDict{{valid_pk_name, target_id}, {"value", static_cast(1234)}})); + mixed_list_values.push_back(Mixed(target_obj.get_obj().get_link())); + + Object::create(c, realm, "TopLevel", + std::any(AnyDict{ + {valid_pk_name, obj_id}, + {"mixed_array", mixed_list_values}, + }), + CreatePolicy::ForceCreate); + realm->commit_transaction(); + CHECK(!wait_for_upload(*realm)); + } + + { + TestAppSession test_session(app_session); + SyncTestFile config(test_session.app(), partition, schema); + auto realm = Realm::get_shared_realm(config); + + CHECK(!wait_for_download(*realm)); + CppContext c(realm); + auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{obj_id}); + auto list = util::any_cast(obj.get_property_value(c, "mixed_array")); + for (size_t idx = 0; idx < list.size(); ++idx) { + Mixed mixed = list.get_any(idx); + if (idx == 3) { + CHECK(mixed.is_type(type_TypedLink)); + auto link = mixed.get(); + auto link_table = realm->read_group().get_table(link.get_table_key()); + CHECK(link_table->get_name() == "class_Target"); + auto link_obj = link_table->get_object(link.get_obj_key()); + CHECK(link_obj.get_primary_key() == target_id); + } + else { + CHECK(mixed == util::any_cast(mixed_list_values[idx])); + } + } + } +} + +TEST_CASE("app: roundtrip values", "[sync][pbs][app][baas]") { + const std::string valid_pk_name = "_id"; + + Schema schema{ + {"TopLevel", + { + {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, + {"decimal", PropertyType::Decimal | PropertyType::Nullable}, + }}, + }; + + auto server_app_config = minimal_app_config("roundtrip_values", schema); + auto app_session = create_app(server_app_config); + auto partition = random_string(100); + + Decimal128 large_significand = Decimal128(70) / Decimal128(1.09); + auto obj_id = ObjectId::gen(); + { + TestAppSession test_session({app_session, nullptr, DeleteApp{false}}); + SyncTestFile config(test_session.app(), partition, schema); + auto realm = Realm::get_shared_realm(config); + + CppContext c(realm); + realm->begin_transaction(); + Object::create(c, realm, "TopLevel", + util::Any(AnyDict{ + {valid_pk_name, obj_id}, + {"decimal", large_significand}, + }), + CreatePolicy::ForceCreate); + realm->commit_transaction(); + CHECK(!wait_for_upload(*realm, std::chrono::seconds(600))); + } + + { + TestAppSession test_session(app_session); + SyncTestFile config(test_session.app(), partition, schema); + auto realm = Realm::get_shared_realm(config); + + CHECK(!wait_for_download(*realm)); + CppContext c(realm); + auto obj = Object::get_for_primary_key(c, realm, "TopLevel", util::Any{obj_id}); + auto val = obj.get_column_value("decimal"); + CHECK(val == large_significand); + } +} + +TEST_CASE("app: upgrade from local to synced realm", "[sync][pbs][app][upgrade][baas]") { + const std::string valid_pk_name = "_id"; + + Schema schema{ + {"origin", + {{valid_pk_name, PropertyType::Int, Property::IsPrimary{true}}, + {"link", PropertyType::Object | PropertyType::Nullable, "target"}, + {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"}}}, + {"target", + {{valid_pk_name, PropertyType::String, Property::IsPrimary{true}}, + {"value", PropertyType::Int}, + {"name", PropertyType::String}}}, + {"other_origin", + {{valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, + {"array", PropertyType::Array | PropertyType::Object, "other_target"}}}, + {"other_target", + {{valid_pk_name, PropertyType::UUID, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}, + {"embedded", ObjectSchema::ObjectType::Embedded, {{"name", PropertyType::String | PropertyType::Nullable}}}, + }; + + /* Create local realm */ + TestFile local_config; + local_config.schema = schema; + auto local_realm = Realm::get_shared_realm(local_config); + { + auto origin = local_realm->read_group().get_table("class_origin"); + auto target = local_realm->read_group().get_table("class_target"); + auto other_origin = local_realm->read_group().get_table("class_other_origin"); + auto other_target = local_realm->read_group().get_table("class_other_target"); + + local_realm->begin_transaction(); + auto o = target->create_object_with_primary_key("Foo").set("name", "Egon"); + // 'embedded_link' property is null. + origin->create_object_with_primary_key(47).set("link", o.get_key()); + // 'embedded_link' property is not null. + auto obj = origin->create_object_with_primary_key(42); + auto col_key = origin->get_column_key("embedded_link"); + obj.create_and_set_linked_object(col_key); + other_target->create_object_with_primary_key(UUID("3b241101-e2bb-4255-8caf-4136c566a961")); + other_origin->create_object_with_primary_key(ObjectId::gen()); + local_realm->commit_transaction(); + } + + /* Create a synced realm and upload some data */ + auto server_app_config = minimal_app_config("upgrade_from_local", schema); + TestAppSession test_session(create_app(server_app_config)); + auto partition = random_string(100); + auto user1 = test_session.app()->backing_store()->get_current_user(); + SyncTestFile config1(user1, partition, schema); + + auto r1 = Realm::get_shared_realm(config1); + + auto origin = r1->read_group().get_table("class_origin"); + auto target = r1->read_group().get_table("class_target"); + auto other_origin = r1->read_group().get_table("class_other_origin"); + auto other_target = r1->read_group().get_table("class_other_target"); + + r1->begin_transaction(); + auto o = target->create_object_with_primary_key("Baa").set("name", "Børge"); + origin->create_object_with_primary_key(47).set("link", o.get_key()); + other_target->create_object_with_primary_key(UUID("01234567-89ab-cdef-edcb-a98765432101")); + other_origin->create_object_with_primary_key(ObjectId::gen()); + r1->commit_transaction(); + CHECK(!wait_for_upload(*r1)); + + /* Copy local realm data over in a synced one*/ + create_user_and_log_in(test_session.app()); + auto user2 = test_session.app()->backing_store()->get_current_user(); + REQUIRE(user1 != user2); + + SyncTestFile config2(user1, partition, schema); + + SharedRealm r2; + SECTION("Copy before connecting to server") { + local_realm->convert(config2); + r2 = Realm::get_shared_realm(config2); + } + + SECTION("Open synced realm first") { + r2 = Realm::get_shared_realm(config2); + CHECK(!wait_for_download(*r2)); + local_realm->convert(config2); + CHECK(!wait_for_upload(*r2)); + } + + CHECK(!wait_for_download(*r2)); + advance_and_notify(*r2); + Group& g = r2->read_group(); + // g.to_json(std::cout); + REQUIRE(g.get_table("class_origin")->size() == 2); + REQUIRE(g.get_table("class_target")->size() == 2); + REQUIRE(g.get_table("class_other_origin")->size() == 2); + REQUIRE(g.get_table("class_other_target")->size() == 2); + + CHECK(!wait_for_upload(*r2)); + CHECK(!wait_for_download(*r1)); + advance_and_notify(*r1); + // r1->read_group().to_json(std::cout); +} + +TEST_CASE("app: set new embedded object", "[sync][pbs][app][baas]") { + const std::string valid_pk_name = "_id"; + + Schema schema{ + {"TopLevel", + { + {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, + {"array_of_objs", PropertyType::Object | PropertyType::Array, "TopLevel_array_of_objs"}, + {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"}, + {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable, + "TopLevel_embedded_dict"}, + }}, + {"TopLevel_array_of_objs", + ObjectSchema::ObjectType::Embedded, + { + {"array", PropertyType::Int | PropertyType::Array}, + }}, + {"TopLevel_embedded_obj", + ObjectSchema::ObjectType::Embedded, + { + {"array", PropertyType::Int | PropertyType::Array}, + }}, + {"TopLevel_embedded_dict", + ObjectSchema::ObjectType::Embedded, + { + {"array", PropertyType::Int | PropertyType::Array}, + }}, + }; + + auto server_app_config = minimal_app_config("set_new_embedded_object", schema); + TestAppSession test_session(create_app(server_app_config)); + auto partition = random_string(100); + + auto array_of_objs_id = ObjectId::gen(); + auto embedded_obj_id = ObjectId::gen(); + auto dict_obj_id = ObjectId::gen(); + + { + SyncTestFile config(test_session.app(), partition, schema); + auto realm = Realm::get_shared_realm(config); + + CppContext c(realm); + realm->begin_transaction(); + auto array_of_objs = + Object::create(c, realm, "TopLevel", + std::any(AnyDict{ + {valid_pk_name, array_of_objs_id}, + {"array_of_objs", AnyVector{AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}, + }), + CreatePolicy::ForceCreate); + + auto embedded_obj = + Object::create(c, realm, "TopLevel", + std::any(AnyDict{ + {valid_pk_name, embedded_obj_id}, + {"embedded_obj", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}, + }), + CreatePolicy::ForceCreate); + + auto dict_obj = Object::create( + c, realm, "TopLevel", + std::any(AnyDict{ + {valid_pk_name, dict_obj_id}, + {"embedded_dict", AnyDict{{"foo", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}}, + }), + CreatePolicy::ForceCreate); + + realm->commit_transaction(); + { + realm->begin_transaction(); + embedded_obj.set_property_value(c, "embedded_obj", + std::any(AnyDict{{ + "array", + AnyVector{INT64_C(3), INT64_C(4)}, + }}), + CreatePolicy::UpdateAll); + realm->commit_transaction(); + } + + { + realm->begin_transaction(); + List array(array_of_objs, array_of_objs.get_object_schema().property_for_name("array_of_objs")); + CppContext c2(realm, &array.get_object_schema()); + array.set(c2, 0, std::any{AnyDict{{"array", AnyVector{INT64_C(5), INT64_C(6)}}}}); + realm->commit_transaction(); + } + + { + realm->begin_transaction(); + object_store::Dictionary dict(dict_obj, dict_obj.get_object_schema().property_for_name("embedded_dict")); + CppContext c2(realm, &dict.get_object_schema()); + dict.insert(c2, "foo", std::any{AnyDict{{"array", AnyVector{INT64_C(7), INT64_C(8)}}}}); + realm->commit_transaction(); + } + CHECK(!wait_for_upload(*realm)); + } + + { + SyncTestFile config(test_session.app(), partition, schema); + auto realm = Realm::get_shared_realm(config); + + CHECK(!wait_for_download(*realm)); + CppContext c(realm); + { + auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{embedded_obj_id}); + auto embedded_obj = util::any_cast(obj.get_property_value(c, "embedded_obj")); + auto array_list = util::any_cast(embedded_obj.get_property_value(c, "array")); + CHECK(array_list.size() == 2); + CHECK(array_list.get(0) == int64_t(3)); + CHECK(array_list.get(1) == int64_t(4)); + } + + { + auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{array_of_objs_id}); + auto embedded_list = util::any_cast(obj.get_property_value(c, "array_of_objs")); + CppContext c2(realm, &embedded_list.get_object_schema()); + auto embedded_array_obj = util::any_cast(embedded_list.get(c2, 0)); + auto array_list = util::any_cast(embedded_array_obj.get_property_value(c2, "array")); + CHECK(array_list.size() == 2); + CHECK(array_list.get(0) == int64_t(5)); + CHECK(array_list.get(1) == int64_t(6)); + } + + { + auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{dict_obj_id}); + object_store::Dictionary dict(obj, obj.get_object_schema().property_for_name("embedded_dict")); + CppContext c2(realm, &dict.get_object_schema()); + auto embedded_obj = util::any_cast(dict.get(c2, "foo")); + auto array_list = util::any_cast(embedded_obj.get_property_value(c2, "array")); + CHECK(array_list.size() == 2); + CHECK(array_list.get(0) == int64_t(7)); + CHECK(array_list.get(1) == int64_t(8)); + } + } +} + +TEST_CASE("app: make distributable client file", "[sync][pbs][app][baas]") { + TestAppSession session; + auto app = session.app(); + + auto schema = get_default_schema(); + SyncTestFile original_config(app, bson::Bson("foo"), schema); + create_user_and_log_in(app); + SyncTestFile target_config(app, bson::Bson("foo"), schema); + + // Create realm file without client file id + { + auto realm = Realm::get_shared_realm(original_config); + + // Write some data + realm->begin_transaction(); + CppContext c; + Object::create(c, realm, "Person", + std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())}, + {"age", INT64_C(64)}, + {"firstName", std::string("Paul")}, + {"lastName", std::string("McCartney")}})); + realm->commit_transaction(); + wait_for_upload(*realm); + wait_for_download(*realm); + + realm->convert(target_config); + + // Write some additional data + realm->begin_transaction(); + Object::create(c, realm, "Dog", + std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())}, + {"breed", std::string("stabyhoun")}, + {"name", std::string("albert")}, + {"realm_id", std::string("foo")}})); + realm->commit_transaction(); + wait_for_upload(*realm); + } + // Starting a new session based on the copy + { + auto realm = Realm::get_shared_realm(target_config); + REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); + REQUIRE(realm->read_group().get_table("class_Dog")->size() == 0); + + // Should be able to download the object created in the source Realm + // after writing the copy + wait_for_download(*realm); + realm->refresh(); + REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); + REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1); + + // Check that we can continue committing to this realm + realm->begin_transaction(); + CppContext c; + Object::create(c, realm, "Dog", + std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())}, + {"breed", std::string("bulldog")}, + {"name", std::string("fido")}, + {"realm_id", std::string("foo")}})); + realm->commit_transaction(); + wait_for_upload(*realm); + } + // Original Realm should be able to read the object which was written to the copy + { + auto realm = Realm::get_shared_realm(original_config); + REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); + REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1); + + wait_for_download(*realm); + realm->refresh(); + REQUIRE(realm->read_group().get_table("class_Person")->size() == 1); + REQUIRE(realm->read_group().get_table("class_Dog")->size() == 2); + } +} + +TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { + auto logger = util::Logger::get_default_logger(); + + const auto schema = get_default_schema(); + + auto get_dogs = [](SharedRealm r) -> Results { + wait_for_upload(*r, std::chrono::seconds(10)); + wait_for_download(*r, std::chrono::seconds(10)); + return Results(r, r->read_group().get_table("class_Dog")); + }; + + auto create_one_dog = [](SharedRealm r) { + r->begin_transaction(); + CppContext c; + Object::create(c, r, "Dog", + std::any(AnyDict{{"_id", std::any(ObjectId::gen())}, + {"breed", std::string("bulldog")}, + {"name", std::string("fido")}}), + CreatePolicy::ForceCreate); + r->commit_transaction(); + }; + + TestAppSession session; + auto app = session.app(); + const auto partition = random_string(100); + + // MARK: Add Objects - + SECTION("Add Objects") { + { + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + + REQUIRE(get_dogs(r).size() == 0); + create_one_dog(r); + REQUIRE(get_dogs(r).size() == 1); + } + + { + create_user_and_log_in(app); + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + REQUIRE(dogs.size() == 1); + REQUIRE(dogs.get(0).get("breed") == "bulldog"); + REQUIRE(dogs.get(0).get("name") == "fido"); + } + } + + SECTION("MemOnly durability") { + { + SyncTestFile config(app, partition, schema); + config.in_memory = true; + config.encryption_key = std::vector(); + + REQUIRE(config.options().durability == DBOptions::Durability::MemOnly); + auto r = Realm::get_shared_realm(config); + + REQUIRE(get_dogs(r).size() == 0); + create_one_dog(r); + REQUIRE(get_dogs(r).size() == 1); + } + + { + create_user_and_log_in(app); + SyncTestFile config(app, partition, schema); + config.in_memory = true; + config.encryption_key = std::vector(); + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + REQUIRE(dogs.size() == 1); + REQUIRE(dogs.get(0).get("breed") == "bulldog"); + REQUIRE(dogs.get(0).get("name") == "fido"); + } + } + + // MARK: Expired Session Refresh - + SECTION("Invalid Access Token is Refreshed") { + { + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + REQUIRE(get_dogs(r).size() == 0); + create_one_dog(r); + REQUIRE(get_dogs(r).size() == 1); + } + + { + create_user_and_log_in(app); + auto user = app->backing_store()->get_current_user(); + // set a bad access token. this will trigger a refresh when the sync session opens + user->update_access_token(encode_fake_jwt("fake_access_token")); + + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + REQUIRE(dogs.size() == 1); + REQUIRE(dogs.get(0).get("breed") == "bulldog"); + REQUIRE(dogs.get(0).get("name") == "fido"); + } + } + + class HookedTransport : public SynchronousTestTransport { + public: + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request_hook) { + request_hook(request); + } + if (simulated_response) { + return completion(*simulated_response); + } + SynchronousTestTransport::send_request_to_server(request, [&](const Response& response) mutable { + if (response_hook) { + response_hook(request, response); + } + completion(response); + }); + } + // Optional handler for the request and response before it is returned to completion + std::function response_hook; + // Optional handler for the request before it is sent to the server + std::function request_hook; + // Optional Response object to return immediately instead of communicating with the server + std::optional simulated_response; + }; + + struct HookedSocketProvider : public sync::websocket::DefaultSocketProvider { + HookedSocketProvider(const std::shared_ptr& logger, const std::string user_agent, + AutoStart auto_start = AutoStart{true}) + : DefaultSocketProvider(logger, user_agent, nullptr, auto_start) + { + } + + std::unique_ptr connect(std::unique_ptr observer, + sync::WebSocketEndpoint&& endpoint) override + { + int status_code = 101; + std::string body; + bool use_simulated_response = websocket_connect_func && websocket_connect_func(status_code, body); + + auto websocket = DefaultSocketProvider::connect(std::move(observer), std::move(endpoint)); + if (use_simulated_response) { + auto default_websocket = static_cast(websocket.get()); + if (default_websocket) + default_websocket->force_handshake_response_for_testing(status_code, body); + } + return websocket; + } + + std::function websocket_connect_func; + }; + + { + std::unique_ptr app_session; + std::string base_file_path = util::make_temp_dir() + random_string(10); + auto redir_transport = std::make_shared(); + AutoVerifiedEmailCredentials creds; + + auto app_config = get_config(redir_transport, session.app_session()); + set_app_config_defaults(app_config, redir_transport); + + util::try_make_dir(base_file_path); + SyncClientConfig sc_config; + sc_config.backing_store_config.base_file_path = base_file_path; + sc_config.backing_store_config.metadata_mode = realm::app::BackingStoreConfig::MetadataMode::NoEncryption; + + // initialize app and sync client + auto redir_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + + SECTION("Test invalid redirect response") { + int request_count = 0; + redir_transport->request_hook = [&](const Request& request) { + if (request_count == 0) { + logger->trace("request.url (%1): %2", request_count, request.url); + redir_transport->simulated_response = { + 301, 0, {{"Content-Type", "application/json"}}, "Some body data"}; + request_count++; + } + else if (request_count == 1) { + logger->trace("request.url (%1): %2", request_count, request.url); + redir_transport->simulated_response = { + 301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"}; + request_count++; + } + }; + + // This will fail due to no Location header + redir_app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(error); + REQUIRE(error->is_client_error()); + REQUIRE(error->code() == ErrorCodes::ClientRedirectError); + REQUIRE(error->reason() == "Redirect response missing location header"); + }); + + // This will fail due to empty Location header + redir_app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(error); + REQUIRE(error->is_client_error()); + REQUIRE(error->code() == ErrorCodes::ClientRedirectError); + REQUIRE(error->reason() == "Redirect response missing location header"); + }); + } + + SECTION("Test redirect response") { + int request_count = 0; + // redirect URL is localhost or 127.0.0.1 depending on what the initial value is + std::string original_host = "localhost:9090"; + std::string redirect_scheme = "http://"; + std::string redirect_host = "127.0.0.1:9090"; + std::string redirect_url = "http://127.0.0.1:9090"; + redir_transport->request_hook = [&](const Request& request) { + logger->trace("Received request[%1]: %2", request_count, request.url); + if (request_count == 0) { + // First request should be to location + REQUIRE(request.url.find("/location") != std::string::npos); + if (request.url.find("https://") != std::string::npos) { + redirect_scheme = "https://"; + } + // using local baas + if (request.url.find("127.0.0.1:9090") != std::string::npos) { + redirect_host = "localhost:9090"; + original_host = "127.0.0.1:9090"; + } + // using baas docker - can't test redirect + else if (request.url.find("mongodb-realm:9090") != std::string::npos) { + redirect_host = "mongodb-realm:9090"; + original_host = "mongodb-realm:9090"; + } + + redirect_url = redirect_scheme + redirect_host; + logger->trace("redirect_url (%1): %2", request_count, redirect_url); + request_count++; + } + else if (request_count == 1) { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(!request.redirect_count); + redir_transport->simulated_response = { + 301, + 0, + {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, + "Some body data"}; + request_count++; + } + else if (request_count == 2) { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request.url.find("somehost:9090") != std::string::npos); + redir_transport->simulated_response = { + 308, 0, {{"Location", redirect_url}, {"Content-Type", "application/json"}}, "Some body data"}; + request_count++; + } + else if (request_count == 3) { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request.url.find(redirect_url) != std::string::npos); + redir_transport->simulated_response = { + 301, + 0, + {{"Location", redirect_scheme + original_host}, {"Content-Type", "application/json"}}, + "Some body data"}; + request_count++; + } + else if (request_count == 4) { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos); + // Let the init_app_metadata request go through + redir_transport->simulated_response.reset(); + request_count++; + } + else if (request_count == 5) { + // This is the original request after the init app metadata + logger->trace("request.url (%1): %2", request_count, request.url); + auto backing_store = redir_app->backing_store(); + REQUIRE(backing_store); + auto app_metadata = backing_store->app_metadata(); + REQUIRE(app_metadata); + logger->trace("Deployment model: %1", app_metadata->deployment_model); + logger->trace("Location: %1", app_metadata->location); + logger->trace("Hostname: %1", app_metadata->hostname); + logger->trace("WS Hostname: %1", app_metadata->ws_hostname); + REQUIRE(app_metadata->hostname.find(original_host) != std::string::npos); + REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos); + redir_transport->simulated_response.reset(); + // Validate the retry count tracked in the original message + REQUIRE(request.redirect_count == 3); + request_count++; + } + }; + + // This will be successful after a couple of retries due to the redirect response + redir_app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(!error); + }); + } + SECTION("Test too many redirects") { + int request_count = 0; + redir_transport->request_hook = [&](const Request& request) { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request_count <= 21); + redir_transport->simulated_response = { + request_count % 2 == 1 ? 308 : 301, + 0, + {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, + "Some body data"}; + request_count++; + }; + + redir_app->log_in_with_credentials( + realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, util::Optional error) { + REQUIRE(!user); + REQUIRE(error); + REQUIRE(error->is_client_error()); + REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects); + REQUIRE(error->reason() == "number of redirections exceeded 20"); + }); + } + SECTION("Test server in maintenance") { + redir_transport->request_hook = [&](const Request&) { + nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"}, + {"error", "This service is currently undergoing maintenance"}, + {"link", "https://link.to/server_logs"}}; + redir_transport->simulated_response = { + 500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()}; + }; + + redir_app->log_in_with_credentials( + realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, util::Optional error) { + REQUIRE(!user); + REQUIRE(error); + REQUIRE(error->is_service_error()); + REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress); + REQUIRE(error->reason() == "This service is currently undergoing maintenance"); + REQUIRE(error->link_to_server_logs == "https://link.to/server_logs"); + REQUIRE(*error->additional_status_code == 500); + }); + } + } + SECTION("Test app redirect with no metadata") { + std::unique_ptr app_session; + std::string base_file_path = util::make_temp_dir() + random_string(10); + auto redir_transport = std::make_shared(); + AutoVerifiedEmailCredentials creds, creds2; + + auto app_config = get_config(redir_transport, session.app_session()); + set_app_config_defaults(app_config, redir_transport); + + util::try_make_dir(base_file_path); + SyncClientConfig sc_config; + sc_config.backing_store_config.base_file_path = base_file_path; + sc_config.backing_store_config.metadata_mode = realm::app::BackingStoreConfig::MetadataMode::NoMetadata; + + // initialize app and sync client + auto redir_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + + int request_count = 0; + // redirect URL is localhost or 127.0.0.1 depending on what the initial value is + std::string original_host = "localhost:9090"; + std::string original_scheme = "http://"; + std::string websocket_url = "ws://some-websocket:9090"; + std::string original_url; + redir_transport->request_hook = [&](const Request& request) { + logger->trace("request.url (%1): %2", request_count, request.url); + if (request_count == 0) { + // First request should be to location + REQUIRE(request.url.find("/location") != std::string::npos); + if (request.url.find("https://") != std::string::npos) { + original_scheme = "https://"; + } + // using local baas + if (request.url.find("127.0.0.1:9090") != std::string::npos) { + original_host = "127.0.0.1:9090"; + } + // using baas docker + else if (request.url.find("mongodb-realm:9090") != std::string::npos) { + original_host = "mongodb-realm:9090"; + } + original_url = original_scheme + original_host; + logger->trace("original_url (%1): %2", request_count, original_url); + } + else if (request_count == 1) { + REQUIRE(!request.redirect_count); + redir_transport->simulated_response = { + 308, + 0, + {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, + "Some body data"}; + } + else if (request_count == 2) { + REQUIRE(request.url.find("http://somehost:9090") != std::string::npos); + REQUIRE(request.url.find("location") != std::string::npos); + // app hostname will be updated via the metadata info + redir_transport->simulated_response = { + static_cast(sync::HTTPStatus::Ok), + 0, + {{"Content-Type", "application/json"}}, + util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%1\",\"ws_" + "hostname\":\"%2\"}", + original_url, websocket_url)}; + } + else { + REQUIRE(request.url.find(original_url) != std::string::npos); + redir_transport->simulated_response.reset(); + } + request_count++; + }; + + // This will be successful after a couple of retries due to the redirect response + redir_app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(!error); + }); + REQUIRE(!redir_app->backing_store()->app_metadata()); // no stored app metadata + REQUIRE(redir_app->sync_manager()->sync_route().find(websocket_url) != std::string::npos); + + // Register another email address and verify location data isn't requested again + request_count = 0; + redir_transport->request_hook = [&](const Request& request) { + logger->trace("request.url (%1): %2", request_count, request.url); + redir_transport->simulated_response.reset(); + REQUIRE(request.url.find("location") == std::string::npos); + request_count++; + }; + + redir_app->provider_client().register_email( + creds2.email, creds2.password, [&](util::Optional error) { + REQUIRE(!error); + }); + } + + SECTION("Test websocket redirect with existing session") { + std::string original_host = "localhost:9090"; + std::string redirect_scheme = "http://"; + std::string websocket_scheme = "ws://"; + std::string redirect_host = "127.0.0.1:9090"; + std::string redirect_url = "http://127.0.0.1:9090"; + + auto redir_transport = std::make_shared(); + auto redir_provider = std::make_shared(logger, ""); + std::mutex logout_mutex; + std::condition_variable logout_cv; + bool logged_out = false; + + // Use the transport to grab the current url so it can be converted + redir_transport->request_hook = [&](const Request& request) { + if (request.url.find("https://") != std::string::npos) { + redirect_scheme = "https://"; + websocket_scheme = "wss://"; + } + // using local baas + if (request.url.find("127.0.0.1:9090") != std::string::npos) { + redirect_host = "localhost:9090"; + original_host = "127.0.0.1:9090"; + } + // using baas docker - can't test redirect + else if (request.url.find("mongodb-realm:9090") != std::string::npos) { + redirect_host = "mongodb-realm:9090"; + original_host = "mongodb-realm:9090"; + } + + redirect_url = redirect_scheme + redirect_host; + logger->trace("redirect_url: %1", redirect_url); + }; + + auto server_app_config = minimal_app_config("websocket_redirect", schema); + TestAppSession test_session({create_app(server_app_config), redir_transport, DeleteApp{true}, + realm::ReconnectMode::normal, redir_provider}); + auto partition = random_string(100); + auto user1 = test_session.app()->backing_store()->get_current_user(); + SyncTestFile r_config(user1, partition, schema); + // Override the default + r_config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + if (error.status == ErrorCodes::AuthError) { + util::format(std::cerr, "Websocket redirect test: User logged out\n"); + std::unique_lock lk(logout_mutex); + logged_out = true; + logout_cv.notify_one(); + return; + } + util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n", + error.status); + abort(); + }; + + auto r = Realm::get_shared_realm(r_config); + + REQUIRE(!wait_for_download(*r)); + + SECTION("Valid websocket redirect") { + auto sync_manager = test_session.app()->sync_manager(); + auto sync_session = sync_manager->get_existing_session(r->config().path); + sync_session->pause(); + + int connect_count = 0; + redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) { + if (connect_count++ > 0) + return false; + + status_code = static_cast(sync::HTTPStatus::PermanentRedirect); + body = ""; + return true; + }; + int request_count = 0; + redir_transport->request_hook = [&](const Request& request) { + logger->trace("request.url (%1): %2", request_count, request.url); + if (request_count++ == 0) { + // First request should be a location request against the original URL + REQUIRE(request.url.find(original_host) != std::string::npos); + REQUIRE(request.url.find("/location") != std::string::npos); + REQUIRE(request.redirect_count == 0); + redir_transport->simulated_response = { + static_cast(sync::HTTPStatus::PermanentRedirect), + 0, + {{"Location", redirect_url}, {"Content-Type", "application/json"}}, + "Some body data"}; + } + else if (request.url.find("/location") != std::string::npos) { + redir_transport->simulated_response = { + static_cast(sync::HTTPStatus::Ok), + 0, + {{"Content-Type", "application/json"}}, + util::format( + "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_" + "hostname\":\"%3%1\"}", + redirect_host, redirect_scheme, websocket_scheme)}; + } + else { + redir_transport->simulated_response.reset(); + } + }; + + SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); + sync_session->resume(); + REQUIRE(!wait_for_download(*r)); + REQUIRE(user1->is_logged_in()); + + // Verify session is using the updated server url from the redirect + auto server_url = sync_session->full_realm_url(); + logger->trace("FULL_REALM_URL: %1", server_url); + REQUIRE((server_url && server_url->find(redirect_host) != std::string::npos)); + } + SECTION("Websocket redirect logs out user") { + auto sync_manager = test_session.app()->sync_manager(); + auto sync_session = sync_manager->get_existing_session(r->config().path); + sync_session->pause(); + + int connect_count = 0; + redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) { + if (connect_count++ > 0) + return false; + + status_code = static_cast(sync::HTTPStatus::MovedPermanently); + body = ""; + return true; + }; + int request_count = 0; + redir_transport->request_hook = [&](const Request& request) { + logger->trace("request.url (%1): %2", request_count, request.url); + if (request_count++ == 0) { + // First request should be a location request against the original URL + REQUIRE(request.url.find(original_host) != std::string::npos); + REQUIRE(request.url.find("/location") != std::string::npos); + REQUIRE(request.redirect_count == 0); + redir_transport->simulated_response = { + static_cast(sync::HTTPStatus::MovedPermanently), + 0, + {{"Location", redirect_url}, {"Content-Type", "application/json"}}, + "Some body data"}; + } + else if (request.url.find("/location") != std::string::npos) { + redir_transport->simulated_response = { + static_cast(sync::HTTPStatus::Ok), + 0, + {{"Content-Type", "application/json"}}, + util::format( + "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_" + "hostname\":\"%3%1\"}", + redirect_host, redirect_scheme, websocket_scheme)}; + } + else if (request.url.find("auth/session") != std::string::npos) { + redir_transport->simulated_response = {static_cast(sync::HTTPStatus::Unauthorized), + 0, + {{"Content-Type", "application/json"}}, + ""}; + } + else { + redir_transport->simulated_response.reset(); + } + }; + + SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); + sync_session->resume(); + REQUIRE(wait_for_download(*r)); + std::unique_lock lk(logout_mutex); + auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { + return logged_out; + }); + REQUIRE(result); + REQUIRE(!user1->is_logged_in()); + } + SECTION("Too many websocket redirects logs out user") { + auto sync_manager = test_session.app()->sync_manager(); + auto sync_session = sync_manager->get_existing_session(r->config().path); + sync_session->pause(); + + int connect_count = 0; + redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) { + if (connect_count++ > 0) + return false; + + status_code = static_cast(sync::HTTPStatus::MovedPermanently); + body = ""; + return true; + }; + int request_count = 0; + const int max_http_redirects = 20; // from app.cpp in object-store + redir_transport->request_hook = [&](const Request& request) { + logger->trace("request.url (%1): %2", request_count, request.url); + if (request_count++ == 0) { + // First request should be a location request against the original URL + REQUIRE(request.url.find(original_host) != std::string::npos); + REQUIRE(request.url.find("/location") != std::string::npos); + REQUIRE(request.redirect_count == 0); + } + if (request.url.find("/location") != std::string::npos) { + // Keep returning the redirected response + REQUIRE(request.redirect_count < max_http_redirects); + redir_transport->simulated_response = { + static_cast(sync::HTTPStatus::MovedPermanently), + 0, + {{"Location", redirect_url}, {"Content-Type", "application/json"}}, + "Some body data"}; + } + else { + // should not get any other types of requests during the test - the log out is local + REQUIRE(false); + } + }; + + SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); + sync_session->resume(); + REQUIRE(wait_for_download(*r)); + std::unique_lock lk(logout_mutex); + auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { + return logged_out; + }); + REQUIRE(result); + REQUIRE(!user1->is_logged_in()); + } + } + + SECTION("Fast clock on client") { + { + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + + REQUIRE(get_dogs(r).size() == 0); + create_one_dog(r); + REQUIRE(get_dogs(r).size() == 1); + } + + auto transport = std::make_shared(); + TestAppSession hooked_session({session.app_session(), transport, DeleteApp{false}}); + auto app = hooked_session.app(); + std::shared_ptr user = app->backing_store()->get_current_user(); + REQUIRE(user); + REQUIRE(!user->access_token_refresh_required()); + // Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locally + // (access tokens have an lifetime of 30 minutes today). + user->set_seconds_to_adjust_time_for_testing(31 * 60); + REQUIRE(user->access_token_refresh_required()); + + // This assumes that we make an http request for the new token while + // already in the WaitingForAccessToken state. + bool seen_waiting_for_access_token = false; + transport->request_hook = [&](const Request&) { + auto user = app->current_user(); + REQUIRE(user); + for (auto& session : app->sync_manager()->get_all_sessions_for(user)) { + // Prior to the fix for #4941, this callback would be called from an infinite loop, always in the + // WaitingForAccessToken state. + if (session->state() == SyncSession::State::WaitingForAccessToken) { + REQUIRE(!seen_waiting_for_access_token); + seen_waiting_for_access_token = true; + } + } + return true; + }; + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + REQUIRE(seen_waiting_for_access_token); + Results dogs = get_dogs(r); + REQUIRE(dogs.size() == 1); + REQUIRE(dogs.get(0).get("breed") == "bulldog"); + REQUIRE(dogs.get(0).get("name") == "fido"); + } + + SECTION("Expired Tokens") { + sync::AccessToken token; + { + std::shared_ptr user = app->backing_store()->get_current_user(); + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + + REQUIRE(get_dogs(r).size() == 0); + create_one_dog(r); + + REQUIRE(get_dogs(r).size() == 1); + sync::AccessToken::ParseError error_state = realm::sync::AccessToken::ParseError::none; + sync::AccessToken::parse(user->access_token(), token, error_state, nullptr); + REQUIRE(error_state == sync::AccessToken::ParseError::none); + REQUIRE(token.timestamp); + REQUIRE(token.expires); + REQUIRE(token.timestamp < token.expires); + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + using namespace std::chrono_literals; + token.expires = std::chrono::system_clock::to_time_t(now - 30s); + REQUIRE(token.expired(now)); + } + + auto transport = std::make_shared(); + TestAppSession hooked_session({session.app_session(), transport, DeleteApp{false}}); + auto app = hooked_session.app(); + std::shared_ptr user = app->backing_store()->get_current_user(); + REQUIRE(user); + REQUIRE(!user->access_token_refresh_required()); + // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. + user->update_access_token(encode_fake_jwt("fake_access_token", token.expires, token.timestamp)); + REQUIRE(user->access_token_refresh_required()); + + SECTION("Expired Access Token is Refreshed") { + // This assumes that we make an http request for the new token while + // already in the WaitingForAccessToken state. + bool seen_waiting_for_access_token = false; + transport->request_hook = [&](const Request&) { + auto user = app->current_user(); + REQUIRE(user); + for (auto& session : app->sync_manager()->get_all_sessions_for(user)) { + if (session->state() == SyncSession::State::WaitingForAccessToken) { + REQUIRE(!seen_waiting_for_access_token); + seen_waiting_for_access_token = true; + } + } + }; + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + REQUIRE(seen_waiting_for_access_token); + Results dogs = get_dogs(r); + REQUIRE(dogs.size() == 1); + REQUIRE(dogs.get(0).get("breed") == "bulldog"); + REQUIRE(dogs.get(0).get("name") == "fido"); + } + + SECTION("User is logged out if the refresh request is denied") { + REQUIRE(user->is_logged_in()); + transport->response_hook = [&](const Request& request, const Response& response) { + auto user = app->backing_store()->get_current_user(); + REQUIRE(user); + // simulate the server denying the refresh + if (request.url.find("/session") != std::string::npos) { + auto& response_ref = const_cast(response); + response_ref.http_status_code = 401; + response_ref.body = "fake: refresh token could not be refreshed"; + } + }; + SyncTestFile config(app, partition, schema); + std::atomic sync_error_handler_called{false}; + config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + sync_error_handler_called.store(true); + REQUIRE(error.status.code() == ErrorCodes::AuthError); + REQUIRE_THAT(std::string{error.status.reason()}, + Catch::Matchers::StartsWith("Unable to refresh the user access token")); + }; + auto r = Realm::get_shared_realm(config); + timed_wait_for([&] { + return sync_error_handler_called.load(); + }); + // the failed refresh logs out the user + REQUIRE(!user->is_logged_in()); + } + + SECTION("User is left logged out if logged out while the refresh is in progress") { + REQUIRE(user->is_logged_in()); + transport->request_hook = [&](const Request&) { + user->log_out(); + }; + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + REQUIRE_FALSE(user->is_logged_in()); + REQUIRE(user->state() == SyncUser::State::LoggedOut); + } + + SECTION("Requests that receive an error are retried on a backoff") { + using namespace std::chrono; + std::vector> response_times; + std::atomic did_receive_valid_token{false}; + constexpr size_t num_error_responses = 6; + + transport->response_hook = [&](const Request& request, const Response& response) { + // simulate the server experiencing an internal server error + if (request.url.find("/session") != std::string::npos) { + if (response_times.size() >= num_error_responses) { + did_receive_valid_token.store(true); + return; + } + auto& response_ref = const_cast(response); + response_ref.http_status_code = 500; + } + }; + transport->request_hook = [&](const Request& request) { + if (!did_receive_valid_token.load() && request.url.find("/session") != std::string::npos) { + response_times.push_back(steady_clock::now()); + } + }; + SyncTestFile config(app, partition, schema); + auto r = Realm::get_shared_realm(config); + create_one_dog(r); + timed_wait_for( + [&] { + return did_receive_valid_token.load(); + }, + 30s); + REQUIRE(user->is_logged_in()); + REQUIRE(response_times.size() >= num_error_responses); + std::vector delay_times; + for (size_t i = 1; i < response_times.size(); ++i) { + delay_times.push_back(duration_cast(response_times[i] - response_times[i - 1]).count()); + } + + // sync delays start at 1000ms minus a random number of up to 25%. + // the subsequent delay is double the previous one minus a random 25% again. + // this calculation happens in Connection::initiate_reconnect_wait() + bool increasing_delay = true; + for (size_t i = 1; i < delay_times.size(); ++i) { + if (delay_times[i - 1] >= delay_times[i]) { + increasing_delay = false; + } + } + // fail if the first delay isn't longer than half a second + if (delay_times.size() <= 1 || delay_times[1] < 500) { + increasing_delay = false; + } + if (!increasing_delay) { + std::cerr << "delay times are not increasing: "; + for (auto& delay : delay_times) { + std::cerr << delay << ", "; + } + std::cerr << std::endl; + } + REQUIRE(increasing_delay); + } + } + + SECTION("Invalid refresh token") { + auto& app_session = session.app_session(); + std::mutex mtx; + auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr user, + Realm::Config config) { + REQUIRE(user); + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + + // requesting a new access token fails because the refresh token used for this request is revoked + user->refresh_custom_data([&](Optional error) { + REQUIRE(error); + REQUIRE(error->additional_status_code == 401); + REQUIRE(error->code() == ErrorCodes::InvalidSession); + }); + + // Set a bad access token. This will force a request for a new access token when the sync session opens + // this is only necessary because the server doesn't actually revoke previously issued access tokens + // instead allowing their session to time out as normal. So this simulates the access token expiring. + // see: + // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386 + user->update_access_token(encode_fake_jwt("fake_access_token")); + REQUIRE(!app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + + auto [sync_error_promise, sync_error] = util::make_promise_future(); + config.sync_config->error_handler = + [promise = util::CopyablePromiseHolder(std::move(sync_error_promise))](std::shared_ptr, + SyncError error) mutable { + promise.get_promise().emplace_value(std::move(error)); + }; + + auto transport = static_cast(session.transport()); + transport->block(); // don't let the token refresh happen until we're ready for it + auto r = Realm::get_shared_realm(config); + auto session = app->sync_manager()->get_existing_session(config.path); + REQUIRE(user->is_logged_in()); + REQUIRE(!sync_error.is_ready()); + { + std::atomic called{false}; + session->wait_for_upload_completion([&](Status stat) { + std::lock_guard lock(mtx); + called.store(true); + REQUIRE(stat.code() == ErrorCodes::InvalidSession); + }); + transport->unblock(); + timed_wait_for([&] { + return called.load(); + }); + std::lock_guard lock(mtx); + REQUIRE(called); + } + + auto sync_error_res = wait_for_future(std::move(sync_error)).get(); + REQUIRE(sync_error_res.status == ErrorCodes::AuthError); + REQUIRE_THAT(std::string{sync_error_res.status.reason()}, + Catch::Matchers::StartsWith("Unable to refresh the user access token")); + + // the failed refresh logs out the user + std::lock_guard lock(mtx); + REQUIRE(!user->is_logged_in()); + }; + + SECTION("Disabled user results in a sync error") { + auto creds = create_user_and_log_in(app); + SyncTestFile config(app, partition, schema); + auto user = app->backing_store()->get_current_user(); + REQUIRE(user); + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + app_session.admin_api.disable_user_sessions(app->current_user()->identity(), app_session.server_app_id); + + verify_error_on_sync_with_invalid_refresh_token(user, config); + + // logging in again doesn't fix things while the account is disabled + auto error = failed_log_in(app, creds); + REQUIRE(error.code() == ErrorCodes::UserDisabled); + + // admin enables user sessions again which should allow the session to continue + app_session.admin_api.enable_user_sessions(user->identity(), app_session.server_app_id); + + // logging in now works properly + log_in(app, creds); + + // still referencing the same user + REQUIRE(user == app->backing_store()->get_current_user()); + REQUIRE(user->is_logged_in()); + + { + // check that there are no errors initiating a session now by making sure upload/download succeeds + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + } + } + + SECTION("Revoked refresh token results in a sync error") { + auto creds = create_user_and_log_in(app); + SyncTestFile config(app, partition, schema); + auto user = app->current_user(); + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + app_session.admin_api.revoke_user_sessions(user->identity(), app_session.server_app_id); + // revoking a user session only affects the refresh token, so the access token should still continue to + // work. + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + + verify_error_on_sync_with_invalid_refresh_token(user, config); + + // logging in again succeeds and generates a new and valid refresh token + log_in(app, creds); + + // still referencing the same user and now the user is logged in + REQUIRE(user == app->backing_store()->get_current_user()); + REQUIRE(user->is_logged_in()); + + // new requests for an access token succeed again + user->refresh_custom_data([&](Optional error) { + REQUIRE_FALSE(error); + }); + + { + // check that there are no errors initiating a new sync session by making sure upload/download + // succeeds + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + } + } + + SECTION("Revoked refresh token on an anonymous user results in a sync error") { + app->current_user()->log_out(); + auto anon_user = log_in(app); + REQUIRE(app->current_user() == anon_user); + SyncTestFile config(app, partition, schema); + REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id)); + app_session.admin_api.revoke_user_sessions(anon_user->identity(), app_session.server_app_id); + // revoking a user session only affects the refresh token, so the access token should still continue to + // work. + REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id)); + + verify_error_on_sync_with_invalid_refresh_token(anon_user, config); + + // the user has been logged out, and current user is reset + REQUIRE(!app->current_user()); + REQUIRE(!anon_user->is_logged_in()); + REQUIRE(anon_user->state() == SyncUser::State::Removed); + + // new requests for an access token do not work for anon users + anon_user->refresh_custom_data([&](Optional error) { + REQUIRE(error); + REQUIRE(error->reason() == + util::format("Cannot initiate a refresh on user '%1' because the user has been removed", + anon_user->identity())); + }); + + REQUIRE_EXCEPTION( + Realm::get_shared_realm(config), ClientUserNotFound, + util::format("Cannot start a sync session for user '%1' because this user has been removed.", + anon_user->identity())); + } + + SECTION("Opening a Realm with a removed email user results produces an exception") { + auto creds = create_user_and_log_in(app); + auto email_user = app->current_user(); + const std::string user_ident = email_user->identity(); + REQUIRE(email_user); + SyncTestFile config(app, partition, schema); + REQUIRE(email_user->is_logged_in()); + { + // sync works on a valid user + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + } + app->backing_store()->remove_user(user_ident); + REQUIRE_FALSE(email_user->is_logged_in()); + REQUIRE(email_user->state() == SyncUser::State::Removed); + + // should not be able to open a synced Realm with an invalid user + REQUIRE_EXCEPTION( + Realm::get_shared_realm(config), ClientUserNotFound, + util::format("Cannot start a sync session for user '%1' because this user has been removed.", + user_ident)); + + std::shared_ptr new_user_instance = log_in(app, creds); + // the previous instance is still invalid + REQUIRE_FALSE(email_user->is_logged_in()); + REQUIRE(email_user->state() == SyncUser::State::Removed); + // but the new instance will work and has the same server issued ident + REQUIRE(new_user_instance); + REQUIRE(new_user_instance->is_logged_in()); + REQUIRE(new_user_instance->identity() == user_ident); + { + // sync works again if the same user is logged back in + config.sync_config->user = new_user_instance; + auto r = Realm::get_shared_realm(config); + Results dogs = get_dogs(r); + } + } + } + + SECTION("large write transactions which would be too large if batched") { + SyncTestFile config(app, partition, schema); + + std::mutex mutex; + bool done = false; + auto r = Realm::get_shared_realm(config); + r->sync_session()->pause(); + + // Create 26 MB worth of dogs in 26 transactions, which should work but + // will result in an error from the server if the changesets are batched + // for upload. + CppContext c; + for (auto i = 'a'; i < 'z'; ++i) { + r->begin_transaction(); + Object::create(c, r, "Dog", + std::any(AnyDict{{"_id", std::any(ObjectId::gen())}, + {"breed", std::string("bulldog")}, + {"name", random_string(1024 * 1024)}}), + CreatePolicy::ForceCreate); + r->commit_transaction(); + } + r->sync_session()->wait_for_upload_completion([&](Status status) { + std::lock_guard lk(mutex); + REQUIRE(status.is_ok()); + done = true; + }); + r->sync_session()->resume(); + + // If we haven't gotten an error in more than 5 minutes, then something has gone wrong + // and we should fail the test. + timed_wait_for( + [&] { + std::lock_guard lk(mutex); + return done; + }, + std::chrono::minutes(5)); + } + + SECTION("too large sync message error handling") { + SyncTestFile config(app, partition, schema); + + auto pf = util::make_promise_future(); + config.sync_config->error_handler = + [sp = util::CopyablePromiseHolder(std::move(pf.promise))](auto, SyncError error) mutable { + sp.get_promise().emplace_value(std::move(error)); + }; + auto r = Realm::get_shared_realm(config); + + // Create 26 MB worth of dogs in a single transaction - this should all get put into one changeset + // and get uploaded at once, which for now is an error on the server. + r->begin_transaction(); + CppContext c; + for (auto i = 'a'; i < 'z'; ++i) { + Object::create(c, r, "Dog", + std::any(AnyDict{{"_id", std::any(ObjectId::gen())}, + {"breed", std::string("bulldog")}, + {"name", random_string(1024 * 1024)}}), + CreatePolicy::ForceCreate); + } + r->commit_transaction(); + +#if defined(TEST_TIMEOUT_EXTRA) && TEST_TIMEOUT_EXTRA > 0 + // It may take 30 minutes to transfer 16MB at 10KB/s + auto delay = std::chrono::minutes(35); +#else + auto delay = std::chrono::minutes(5); +#endif + + auto error = wait_for_future(std::move(pf.future), delay).get(); + REQUIRE(error.status == ErrorCodes::LimitExceeded); + REQUIRE(error.status.reason() == + "Sync websocket closed because the server received a message that was too large: " + "read limited at 16777217 bytes"); + REQUIRE(error.is_client_reset_requested()); + REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset); + } + + SECTION("freezing realm does not resume session") { + SyncTestFile config(app, partition, schema); + auto realm = Realm::get_shared_realm(config); + wait_for_download(*realm); + + auto state = realm->sync_session()->state(); + REQUIRE(state == SyncSession::State::Active); + + realm->sync_session()->pause(); + state = realm->sync_session()->state(); + REQUIRE(state == SyncSession::State::Paused); + + realm->read_group(); + + { + auto frozen = realm->freeze(); + REQUIRE(realm->sync_session() == realm->sync_session()); + REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused); + } + + { + auto frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()); + REQUIRE(realm->sync_session() == realm->sync_session()); + REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused); + } + } + + SECTION("pausing a session does not hold the DB open") { + SyncTestFile config(app, partition, schema); + DBRef dbref; + std::shared_ptr sync_sess_ext_ref; + { + auto realm = Realm::get_shared_realm(config); + wait_for_download(*realm); + + auto state = realm->sync_session()->state(); + REQUIRE(state == SyncSession::State::Active); + + sync_sess_ext_ref = realm->sync_session()->external_reference(); + dbref = TestHelper::get_db(*realm); + // One ref each for the + // - RealmCoordinator + // - SyncSession + // - SessionWrapper + // - local dbref + REQUIRE(dbref.use_count() >= 4); + + realm->sync_session()->pause(); + state = realm->sync_session()->state(); + REQUIRE(state == SyncSession::State::Paused); + } + + // Closing the realm should leave one ref for the SyncSession and one for the local dbref. + REQUIRE_THAT( + [&] { + return dbref.use_count() < 4; + }, + ReturnsTrueWithinTimeLimit{}); + + // Releasing the external reference should leave one ref (the local dbref) only. + sync_sess_ext_ref.reset(); + REQUIRE_THAT( + [&] { + return dbref.use_count() == 1; + }, + ReturnsTrueWithinTimeLimit{}); + } + + SECTION("validation") { + SyncTestFile config(app, partition, schema); + + SECTION("invalid partition error handling") { + config.sync_config->partition_value = "not a bson serialized string"; + std::atomic error_did_occur = false; + config.sync_config->error_handler = [&error_did_occur](std::shared_ptr, SyncError error) { + CHECK(error.status.reason().find( + "Illegal Realm path (BIND): serialized partition 'not a bson serialized " + "string' is invalid") != std::string::npos); + error_did_occur.store(true); + }; + auto r = Realm::get_shared_realm(config); + auto session = app->sync_manager()->get_existing_session(r->config().path); + timed_wait_for([&] { + return error_did_occur.load(); + }); + REQUIRE(error_did_occur.load()); + } + + SECTION("invalid pk schema error handling") { + const std::string invalid_pk_name = "my_primary_key"; + auto it = config.schema->find("Dog"); + REQUIRE(it != config.schema->end()); + REQUIRE(it->primary_key_property()); + REQUIRE(it->primary_key_property()->name == "_id"); + it->primary_key_property()->name = invalid_pk_name; + it->primary_key = invalid_pk_name; + REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), + "The primary key property on a synchronized Realm must be named '_id' but " + "found 'my_primary_key' for type 'Dog'"); + } + + SECTION("missing pk schema error handling") { + auto it = config.schema->find("Dog"); + REQUIRE(it != config.schema->end()); + REQUIRE(it->primary_key_property()); + it->primary_key_property()->is_primary = false; + it->primary_key = ""; + REQUIRE(!it->primary_key_property()); + REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), + "There must be a primary key property named '_id' on a synchronized " + "Realm but none was found for type 'Dog'"); + } + } +} + +namespace cf = realm::collection_fixtures; +TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects, + cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects, + cf::DictionaryOfMixedLinks) +{ + const std::string valid_pk_name = "_id"; + const auto partition = random_string(100); + TestType test_type("collection", "dest"); + Schema schema = {{"source", + {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true}, + {"realm_id", PropertyType::String | PropertyType::Nullable}, + test_type.property()}}, + {"dest", + { + {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true}, + {"realm_id", PropertyType::String | PropertyType::Nullable}, + }}}; + auto server_app_config = minimal_app_config("collections_of_links", schema); + TestAppSession test_session(create_app(server_app_config)); + + auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) { + timed_sleeping_wait_for([&]() -> bool { + r->refresh(); + TableRef dest = r->read_group().get_table(table_name); + size_t cur_count = dest->size(); + return cur_count == count; + }); + }; + auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) { + timed_sleeping_wait_for([&]() -> bool { + r->refresh(); + return test_type.size_of_collection(obj) == count; + }); + }; + + CppContext c; + auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector links = {}) { + r->begin_transaction(); + auto object = Object::create( + c, r, "source", + std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}), + CreatePolicy::ForceCreate); + + for (auto link : links) { + auto& obj = object.get_obj(); + test_type.add_link(obj, link); + } + r->commit_transaction(); + }; + + auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink { + r->begin_transaction(); + auto obj = Object::create( + c, r, "dest", + std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}), + CreatePolicy::ForceCreate); + r->commit_transaction(); + return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()}; + }; + + auto require_links_to_match_ids = [&](std::vector links, std::vector expected) { + std::vector actual; + for (auto obj : links) { + actual.push_back(obj.get(valid_pk_name)); + } + std::sort(actual.begin(), actual.end()); + std::sort(expected.begin(), expected.end()); + REQUIRE(actual == expected); + }; + + SECTION("integration testing") { + auto app = test_session.app(); + SyncTestFile config1(app, partition, schema); // uses the current user created above + auto r1 = realm::Realm::get_shared_realm(config1); + Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source")); + + create_user_and_log_in(app); + SyncTestFile config2(app, partition, schema); // uses the user created above + auto r2 = realm::Realm::get_shared_realm(config2); + Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source")); + + constexpr int64_t source_pk = 0; + constexpr int64_t dest_pk_1 = 1; + constexpr int64_t dest_pk_2 = 2; + constexpr int64_t dest_pk_3 = 3; + { // add a container collection with three valid links + REQUIRE(r1_source_objs.size() == 0); + ObjLink dest1 = create_one_dest_object(r1, dest_pk_1); + ObjLink dest2 = create_one_dest_object(r1, dest_pk_2); + ObjLink dest3 = create_one_dest_object(r1, dest_pk_3); + create_one_source_object(r1, source_pk, {dest1, dest2, dest3}); + REQUIRE(r1_source_objs.size() == 1); + REQUIRE(r1_source_objs.get(0).get(valid_pk_name) == source_pk); + REQUIRE(r1_source_objs.get(0).get("realm_id") == partition); + require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3}); + } + + size_t expected_coll_size = 3; + std::vector remaining_dest_object_ids; + { // erase one of the destination objects + wait_for_num_objects_to_equal(r2, "class_source", 1); + wait_for_num_objects_to_equal(r2, "class_dest", 3); + REQUIRE(r2_source_objs.size() == 1); + REQUIRE(r2_source_objs.get(0).get(valid_pk_name) == source_pk); + REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3); + auto linked_objects = test_type.get_links(r2_source_objs.get(0)); + require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3}); + r2->begin_transaction(); + linked_objects[0].remove(); + r2->commit_transaction(); + remaining_dest_object_ids = {linked_objects[1].template get(valid_pk_name), + linked_objects[2].template get(valid_pk_name)}; + expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3; + REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size); + } + + { // remove a link from the collection + wait_for_num_objects_to_equal(r1, "class_dest", 2); + REQUIRE(r1_source_objs.size() == 1); + REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size); + auto linked_objects = test_type.get_links(r1_source_objs.get(0)); + require_links_to_match_ids(linked_objects, remaining_dest_object_ids); + r1->begin_transaction(); + auto obj = r1_source_objs.get(0); + test_type.remove_link(obj, + ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()}); + r1->commit_transaction(); + --expected_coll_size; + remaining_dest_object_ids = {linked_objects[1].template get(valid_pk_name)}; + REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size); + } + + { // clear the collection + REQUIRE(r2_source_objs.size() == 1); + REQUIRE(r2_source_objs.get(0).get(valid_pk_name) == source_pk); + wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size); + auto linked_objects = test_type.get_links(r2_source_objs.get(0)); + require_links_to_match_ids(linked_objects, remaining_dest_object_ids); + r2->begin_transaction(); + test_type.clear_collection(r2_source_objs.get(0)); + r2->commit_transaction(); + expected_coll_size = 0; + REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size); + } + + { // expect an empty collection + REQUIRE(r1_source_objs.size() == 1); + wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size); + } + } +} + +TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID, + cf::UUID, cf::BoxedOptional, cf::UnboxedOptional, cf::BoxedOptional, + cf::BoxedOptional) +{ + const std::string valid_pk_name = "_id"; + const std::string partition_key_col_name = "partition_key_prop"; + const std::string table_name = "class_partition_test_type"; + auto partition_property = Property(partition_key_col_name, TestType::property_type); + Schema schema = {{Group::table_name_to_class_name(table_name), + { + {valid_pk_name, PropertyType::Int, true}, + partition_property, + }}}; + auto server_app_config = minimal_app_config("partition_types_app_name", schema); + server_app_config.partition_key = partition_property; + TestAppSession test_session(create_app(server_app_config)); + auto app = test_session.app(); + + auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) { + timed_sleeping_wait_for([&]() -> bool { + r->refresh(); + TableRef dest = r->read_group().get_table(table_name); + size_t cur_count = dest->size(); + return cur_count == count; + }); + }; + using T = typename TestType::Type; + CppContext c; + auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) { + r->begin_transaction(); + auto object = Object::create( + c, r, Group::table_name_to_class_name(table_name), + std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}), + CreatePolicy::ForceCreate); + r->commit_transaction(); + }; + + auto get_bson = [](T val) -> bson::Bson { + if constexpr (std::is_same_v) { + return val.is_null() ? bson::Bson(util::none) : bson::Bson(val); + } + else if constexpr (TestType::is_optional) { + return val ? bson::Bson(*val) : bson::Bson(util::none); + } + else { + return bson::Bson(val); + } + }; + + SECTION("can round trip an object") { + auto values = TestType::values(); + auto user1 = app->current_user(); + create_user_and_log_in(app); + auto user2 = app->current_user(); + REQUIRE(user1); + REQUIRE(user2); + REQUIRE(user1 != user2); + for (T partition_value : values) { + SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above + auto r1 = realm::Realm::get_shared_realm(config1); + Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name)); + + SyncTestFile config2(user2, get_bson(partition_value), schema); // uses the user created above + auto r2 = realm::Realm::get_shared_realm(config2); + Results r2_source_objs = realm::Results(r2, r2->read_group().get_table(table_name)); + + const int64_t pk_value = random_int(); + { + REQUIRE(r1_source_objs.size() == 0); + create_object(r1, pk_value, TestType::to_any(partition_value)); + REQUIRE(r1_source_objs.size() == 1); + REQUIRE(r1_source_objs.get(0).get(partition_key_col_name) == partition_value); + REQUIRE(r1_source_objs.get(0).get(valid_pk_name) == pk_value); + } + { + wait_for_num_objects_to_equal(r2, table_name, 1); + REQUIRE(r2_source_objs.size() == 1); + REQUIRE(r2_source_objs.size() == 1); + REQUIRE(r2_source_objs.get(0).get(partition_key_col_name) == partition_value); + REQUIRE(r2_source_objs.get(0).get(valid_pk_name) == pk_value); + } + } + } +} + +TEST_CASE("app: full-text compatible with sync", "[sync][app][baas]") { + const std::string valid_pk_name = "_id"; + + Schema schema{ + {"TopLevel", + { + {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}}, + {"full_text", Property::IsFulltextIndexed{true}}, + }}, + }; + + auto server_app_config = minimal_app_config("full_text", schema); + auto app_session = create_app(server_app_config); + const auto partition = random_string(100); + TestAppSession test_session({app_session, nullptr}); + SyncTestFile config(test_session.app(), partition, schema); + SharedRealm realm; + SECTION("sync open") { + INFO("realm opened without async open"); + realm = Realm::get_shared_realm(config); + } + SECTION("async open") { + INFO("realm opened with async open"); + auto async_open_task = Realm::get_synchronized_realm(config); + + auto [realm_promise, realm_future] = util::make_promise_future(); + async_open_task->start( + [promise = std::move(realm_promise)](ThreadSafeReference ref, std::exception_ptr ouch) mutable { + if (ouch) { + try { + std::rethrow_exception(ouch); + } + catch (...) { + promise.set_error(exception_to_status()); + } + } + else { + promise.emplace_value(std::move(ref)); + } + }); + + realm = Realm::get_shared_realm(std::move(realm_future.get())); + } + + CppContext c(realm); + auto obj_id_1 = ObjectId::gen(); + auto obj_id_2 = ObjectId::gen(); + realm->begin_transaction(); + Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}})); + Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}})); + realm->commit_transaction(); + + auto table = realm->read_group().get_table("class_TopLevel"); + REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext); + Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world")); + REQUIRE(world_results.size() == 1); + REQUIRE(world_results.get(0).get_primary_key() == Mixed{obj_id_1}); +} + +namespace { +class AsyncMockNetworkTransport { +public: + AsyncMockNetworkTransport() + : transport_thread(&AsyncMockNetworkTransport::worker_routine, this) + { + } + + void add_work_item(Response&& response, util::UniqueFunction&& completion) + { + std::lock_guard lk(transport_work_mutex); + transport_work.push_front(ResponseWorkItem{std::move(response), std::move(completion)}); + transport_work_cond.notify_one(); + } + + void add_work_item(util::UniqueFunction cb) + { + std::lock_guard lk(transport_work_mutex); + transport_work.push_front(std::move(cb)); + transport_work_cond.notify_one(); + } + + void mark_complete() + { + std::unique_lock lk(transport_work_mutex); + test_complete = true; + transport_work_cond.notify_one(); + lk.unlock(); + transport_thread.join(); + } + +private: + struct ResponseWorkItem { + Response response; + util::UniqueFunction completion; + }; + + void worker_routine() + { + std::unique_lock lk(transport_work_mutex); + for (;;) { + transport_work_cond.wait(lk, [&] { + return test_complete || !transport_work.empty(); + }); + + if (!transport_work.empty()) { + auto work_item = std::move(transport_work.back()); + transport_work.pop_back(); + lk.unlock(); + + mpark::visit(util::overload{[](ResponseWorkItem& work_item) { + work_item.completion(std::move(work_item.response)); + }, + [](util::UniqueFunction& cb) { + cb(); + }}, + work_item); + + lk.lock(); + continue; + } + + if (test_complete) { + return; + } + } + } + + std::mutex transport_work_mutex; + std::condition_variable transport_work_cond; + bool test_complete = false; + std::list>> transport_work; + JoiningThread transport_thread; +}; + +} // namespace + +#if 0 +TEST_CASE("app: app cannot get deallocated during log in", "[sync][app]") { + AsyncMockNetworkTransport mock_transport_worker; + enum class TestState { unknown, location, login, app_deallocated, profile }; + struct TestStateBundle { + void advance_to(TestState new_state) + { + std::lock_guard lk(mutex); + state = new_state; + cond.notify_one(); + } + + TestState get() const + { + std::lock_guard lk(mutex); + return state; + } + + void wait_for(TestState new_state) + { + std::unique_lock lk(mutex); + cond.wait(lk, [&] { + return state == new_state; + }); + } + + mutable std::mutex mutex; + std::condition_variable cond; + + TestState state = TestState::unknown; + } state; + struct transport : public GenericNetworkTransport { + transport(AsyncMockNetworkTransport& worker, TestStateBundle& state) + : mock_transport_worker(worker) + , state(state) + { + } + + void send_request_to_server(const Request& request, util::UniqueFunction&& completion) override + { + if (request.url.find("/login") != std::string::npos) { + state.advance_to(TestState::login); + state.wait_for(TestState::app_deallocated); + mock_transport_worker.add_work_item( + Response{200, 0, {}, user_json(encode_fake_jwt("access token")).dump()}, + std::move(completion)); + } + else if (request.url.find("/profile") != std::string::npos) { + state.advance_to(TestState::profile); + mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()}, + std::move(completion)); + } + else if (request.url.find("/location") != std::string::npos) { + CHECK(request.method == HttpMethod::get); + state.advance_to(TestState::location); + mock_transport_worker.add_work_item( + Response{200, + 0, + {}, + "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" + "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}, + std::move(completion)); + } + } + + AsyncMockNetworkTransport& mock_transport_worker; + TestStateBundle& state; + }; + + auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); + auto transporter = std::make_shared(mock_transport_worker, state); + + { + TestSyncManager sync_manager(get_config(transporter)); + auto app = sync_manager.app(); + + app->log_in_with_credentials(AppCredentials::anonymous(), + [promise = std::move(cur_user_promise)](std::shared_ptr user, + util::Optional error) mutable { + REQUIRE_FALSE(error); + promise.emplace_value(std::move(user)); + }); + } + + // At this point the test does not hold any reference to `app`. + state.advance_to(TestState::app_deallocated); + auto cur_user = std::move(cur_user_future).get(); + CHECK(cur_user); + + mock_transport_worker.mark_complete(); +} +#endif + +TEST_CASE("app: user logs out while profile is fetched", "[sync][app][user]") { + AsyncMockNetworkTransport mock_transport_worker; + enum class TestState { unknown, location, login, profile }; + struct TestStateBundle { + void advance_to(TestState new_state) + { + std::lock_guard lk(mutex); + state = new_state; + cond.notify_one(); + } + + TestState get() const + { + std::lock_guard lk(mutex); + return state; + } + + void wait_for(TestState new_state) + { + std::unique_lock lk(mutex); + cond.wait(lk, [&] { + return state == new_state; + }); + } + + mutable std::mutex mutex; + std::condition_variable cond; + + TestState state = TestState::unknown; + } state; + struct transport : public GenericNetworkTransport { + transport(AsyncMockNetworkTransport& worker, TestStateBundle& state, + std::shared_ptr& logged_in_user) + : mock_transport_worker(worker) + , state(state) + , logged_in_user(logged_in_user) + { + } + + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/login") != std::string::npos) { + state.advance_to(TestState::login); + mock_transport_worker.add_work_item( + Response{200, 0, {}, user_json(encode_fake_jwt("access token")).dump()}, std::move(completion)); + } + else if (request.url.find("/profile") != std::string::npos) { + logged_in_user->log_out(); + state.advance_to(TestState::profile); + mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()}, + std::move(completion)); + } + else if (request.url.find("/location") != std::string::npos) { + CHECK(request.method == HttpMethod::get); + state.advance_to(TestState::location); + mock_transport_worker.add_work_item( + Response{200, + 0, + {}, + "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" + "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}, + std::move(completion)); + } + } + + AsyncMockNetworkTransport& mock_transport_worker; + TestStateBundle& state; + std::shared_ptr& logged_in_user; + }; + + std::shared_ptr logged_in_user; + auto transporter = std::make_shared(mock_transport_worker, state, logged_in_user); + OfflineAppSession tas({transporter}); + auto app = tas.app(); + + logged_in_user = app->backing_store()->get_user("userid", good_access_token, good_access_token, dummy_device_id); + auto custom_credentials = AppCredentials::facebook("a_token"); + auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); + + app->link_user(logged_in_user, custom_credentials, + [promise = std::move(cur_user_promise)](std::shared_ptr user, + util::Optional error) mutable { + REQUIRE_FALSE(error); + promise.emplace_value(std::move(user)); + }); + + auto cur_user = std::move(cur_user_future).get(); + CHECK(state.get() == TestState::profile); + CHECK(cur_user); + CHECK(cur_user == logged_in_user); + + mock_transport_worker.mark_complete(); +} + +TEST_CASE("app: app destroyed during token refresh", "[sync][app][user][token]") { + AsyncMockNetworkTransport mock_transport_worker; + enum class TestState { unknown, location, login, profile_1, profile_2, refresh_1, refresh_2, refresh_3 }; + struct TestStateBundle { + void advance_to(TestState new_state) + { + std::lock_guard lk(mutex); + state = new_state; + cond.notify_one(); + } + + TestState get() const + { + std::lock_guard lk(mutex); + return state; + } + + void wait_for(TestState new_state) + { + std::unique_lock lk(mutex); + bool failed = !cond.wait_for(lk, std::chrono::seconds(5), [&] { + return state == new_state; + }); + if (failed) { + throw std::runtime_error("wait timed out"); + } + } + + mutable std::mutex mutex; + std::condition_variable cond; + + TestState state = TestState::unknown; + } state; + struct transport : public GenericNetworkTransport { + transport(AsyncMockNetworkTransport& worker, TestStateBundle& state) + : mock_transport_worker(worker) + , state(state) + { + } + + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/login") != std::string::npos) { + CHECK(state.get() == TestState::location); + state.advance_to(TestState::login); + mock_transport_worker.add_work_item( + Response{200, 0, {}, user_json(encode_fake_jwt("access token 1")).dump()}, std::move(completion)); + } + else if (request.url.find("/profile") != std::string::npos) { + // simulated bad token request + auto cur_state = state.get(); + CHECK((cur_state == TestState::refresh_1 || cur_state == TestState::login)); + if (cur_state == TestState::refresh_1) { + state.advance_to(TestState::profile_2); + mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()}, + std::move(completion)); + } + else if (cur_state == TestState::login) { + state.advance_to(TestState::profile_1); + mock_transport_worker.add_work_item(Response{401, 0, {}}, std::move(completion)); + } + } + else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { + if (state.get() == TestState::profile_1) { + state.advance_to(TestState::refresh_1); + nlohmann::json json{{"access_token", encode_fake_jwt("access token 1")}}; + mock_transport_worker.add_work_item(Response{200, 0, {}, json.dump()}, std::move(completion)); + } + else if (state.get() == TestState::profile_2) { + state.advance_to(TestState::refresh_2); + mock_transport_worker.add_work_item(Response{200, 0, {}, "{\"error\":\"too bad, buddy!\"}"}, + std::move(completion)); + } + else { + CHECK(state.get() == TestState::refresh_2); + state.advance_to(TestState::refresh_3); + nlohmann::json json{{"access_token", encode_fake_jwt("access token 2")}}; + mock_transport_worker.add_work_item(Response{200, 0, {}, json.dump()}, std::move(completion)); + } + } + else if (request.url.find("/location") != std::string::npos) { + CHECK(request.method == HttpMethod::get); + CHECK(state.get() == TestState::unknown); + state.advance_to(TestState::location); + mock_transport_worker.add_work_item( + Response{200, + 0, + {}, + "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" + "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"}, + std::move(completion)); + } + } + + AsyncMockNetworkTransport& mock_transport_worker; + TestStateBundle& state; + }; + TestSyncManager sync_manager(get_config(std::make_shared(mock_transport_worker, state))); + auto app = sync_manager.app(); + + { + auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); + app->log_in_with_credentials(AppCredentials::anonymous(), + [promise = std::move(cur_user_promise)](std::shared_ptr user, + util::Optional error) mutable { + REQUIRE_FALSE(error); + promise.emplace_value(std::move(user)); + }); + + auto cur_user = std::move(cur_user_future).get(); + CHECK(cur_user); + + SyncTestFile config(app->current_user(), bson::Bson("foo")); + // Ignore websocket errors, since sometimes a websocket connection gets started during the test + config.sync_config->error_handler = [](std::shared_ptr session, SyncError error) mutable { + // Ignore these errors, since there's not really an app out there... + // Primarily make sure we don't crash unexpectedly + std::vector expected_errors = {"Bad WebSocket", "Connection Failed", "user has been removed", + "Connection refused", "The user is not logged in"}; + auto expected = + std::find_if(expected_errors.begin(), expected_errors.end(), [error](const char* err_msg) { + return error.status.reason().find(err_msg) != std::string::npos; + }); + if (expected != expected_errors.end()) { + util::format(std::cerr, + "An expected possible WebSocket error was caught during test: 'app destroyed during " + "token refresh': '%1' for '%2'", + error.status, session->path()); + } + else { + std::string err_msg(util::format("An unexpected sync error was caught during test: 'app destroyed " + "during token refresh': '%1' for '%2'", + error.status, session->path())); + std::cerr << err_msg << std::endl; + throw std::runtime_error(err_msg); + } + }; + auto r = Realm::get_shared_realm(config); + auto session = r->sync_session(); + mock_transport_worker.add_work_item([session] { + session->initiate_access_token_refresh(); + }); + } + for (const auto& user : app->all_users()) { + user->log_out(); + } + + timed_wait_for([&] { + return !app->sync_manager()->has_existing_sessions(); + }); + + mock_transport_worker.mark_complete(); +} + +TEST_CASE("app: metadata is persisted between sessions", "[sync][app][metadata]") { + static const auto test_hostname = "proto://host:1234"; + static const auto test_ws_hostname = "wsproto://host:1234"; + + struct transport : UnitTestTransport { + void send_request_to_server(const Request& request, + util::UniqueFunction&& completion) override + { + if (request.url.find("/location") != std::string::npos) { + CHECK(request.method == HttpMethod::get); + completion({200, + 0, + {}, + nlohmann::json({{"deployment_model", "LOCAL"}, + {"location", "IE"}, + {"hostname", test_hostname}, + {"ws_hostname", test_ws_hostname}}) + .dump()}); + } + else if (request.url.find("functions/call") != std::string::npos) { + REQUIRE(request.url.rfind(test_hostname, 0) != std::string::npos); + } + else { + UnitTestTransport::send_request_to_server(request, std::move(completion)); + } + } + }; + + TestSyncManager::Config config = get_config(instance_of); + config.base_path = util::make_temp_dir(); + config.should_teardown_test_directory = false; + + { + TestSyncManager sync_manager(config, {}); + auto app = sync_manager.app(); + app->log_in_with_credentials(AppCredentials::anonymous(), [](auto, auto error) { + REQUIRE_FALSE(error); + }); + REQUIRE(app->sync_manager()->sync_route().rfind(test_ws_hostname, 0) != std::string::npos); + } + + App::clear_cached_apps(); + config.override_sync_route = false; + config.should_teardown_test_directory = true; + { + TestSyncManager sync_manager(config); + auto app = sync_manager.app(); + REQUIRE(app->sync_manager()->sync_route().rfind(test_ws_hostname, 0) != std::string::npos); + app->call_function("function", {}, [](auto error, auto) { + REQUIRE_FALSE(error); + }); + } +} diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index e8332c2443c..38b63972a40 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -1912,20 +1912,21 @@ TEST_CASE("sync: Client reset during async open", "[sync][pbs][client reset][baa }; auto realm_task = Realm::get_synchronized_realm(realm_config); - auto realm_pf = util::make_promise_future(); + auto realm_pf = util::make_promise_future(); realm_task->start([&](ThreadSafeReference ref, std::exception_ptr ex) { try { if (ex) { std::rethrow_exception(ex); } - auto realm = Realm::get_shared_realm(std::move(ref)); - realm_pf.promise.emplace_value(std::move(realm)); + realm_pf.promise.emplace_value(std::move(ref)); } catch (...) { realm_pf.promise.set_error(exception_to_status()); } }); - auto realm = realm_pf.future.get(); + auto realm = Realm::get_shared_realm(std::move(realm_pf.future.get())); + REQUIRE(realm); + realm->sync_session()->shutdown_and_wait(); before_callback_called.future.get(); after_callback_called.future.get(); } diff --git a/test/object-store/sync/session/connection_change_notifications.cpp b/test/object-store/sync/session/connection_change_notifications.cpp index 121398342cc..e972b0d3541 100644 --- a/test/object-store/sync/session/connection_change_notifications.cpp +++ b/test/object-store/sync/session/connection_change_notifications.cpp @@ -41,8 +41,6 @@ using namespace realm; using namespace realm::util; -static const std::string dummy_device_id = "123400000000000000000000"; - static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_connection_state_changes"; TEST_CASE("sync: Connection state changes", "[sync][session][connection change]") { diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index 99cdbe5bbf0..568b07e575e 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -41,8 +41,6 @@ using namespace realm; using namespace realm::util; -static const std::string dummy_device_id = "123400000000000000000000"; - static std::shared_ptr get_user(const std::shared_ptr& app) { return app->backing_store()->get_user("user_id", ENCODE_FAKE_JWT("fake_refresh_token"), diff --git a/test/object-store/sync/sync_manager.cpp b/test/object-store/sync/sync_manager.cpp index c18662c35b9..20adec8e3b5 100644 --- a/test/object-store/sync/sync_manager.cpp +++ b/test/object-store/sync/sync_manager.cpp @@ -36,7 +36,6 @@ using namespace realm::util; using File = realm::util::File; static const auto base_path = fs::path{util::make_temp_dir()}.make_preferred() / "realm_objectstore_sync_manager"; -static const std::string dummy_device_id = "123400000000000000000000"; namespace { bool validate_user_in_vector(std::vector> vector, const std::string& identity, diff --git a/test/object-store/sync/user.cpp b/test/object-store/sync/user.cpp index 7eba77c5046..9d1f80322b7 100644 --- a/test/object-store/sync/user.cpp +++ b/test/object-store/sync/user.cpp @@ -31,7 +31,6 @@ using namespace realm::util; using File = realm::util::File; static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_user/"; -static const std::string dummy_device_id = "123400000000000000000000"; TEST_CASE("sync_user: SyncManager `get_user()` API", "[sync][user]") { TestSyncManager init_sync_manager; diff --git a/test/object-store/util/sync/flx_sync_harness.hpp b/test/object-store/util/sync/flx_sync_harness.hpp index ffe3eb1cffc..8fdd8a339fe 100644 --- a/test/object-store/util/sync/flx_sync_harness.hpp +++ b/test/object-store/util/sync/flx_sync_harness.hpp @@ -78,16 +78,18 @@ class FLXSyncTestHarness { }; explicit FLXSyncTestHarness(Config&& config) - : m_test_session(make_app_from_server_schema(config.test_name, config.server_schema), config.transport, true, - config.reconnect_mode, config.custom_socket_provider) + : m_test_session(TestAppSession::Config{make_app_from_server_schema(config.test_name, config.server_schema), + config.transport, true, config.reconnect_mode, + config.custom_socket_provider}) , m_schema(std::move(config.server_schema.schema)) { } FLXSyncTestHarness(const std::string& test_name, ServerSchema server_schema = default_server_schema(), std::shared_ptr transport = instance_of, std::shared_ptr custom_socket_provider = nullptr) - : m_test_session(make_app_from_server_schema(test_name, server_schema), std::move(transport), true, - realm::ReconnectMode::normal, custom_socket_provider) + : m_test_session(TestAppSession::Config{make_app_from_server_schema(test_name, server_schema), + std::move(transport), true, realm::ReconnectMode::normal, + custom_socket_provider}) , m_schema(std::move(server_schema.schema)) { } diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index 586ed3657ad..183c53e2b4d 100644 --- a/test/object-store/util/sync/sync_test_utils.cpp +++ b/test/object-store/util/sync/sync_test_utils.cpp @@ -19,6 +19,7 @@ #include "util/sync/sync_test_utils.hpp" #include "util/sync/baas_admin_api.hpp" +#include "util/unit_test_transport.hpp" #include #include @@ -236,6 +237,35 @@ AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app) return creds; } +std::shared_ptr log_in(std::shared_ptr app, app::AppCredentials credentials) +{ + if (auto transport = dynamic_cast(app->config().transport.get())) { + transport->set_provider_type(credentials.provider_as_string()); + } + std::shared_ptr user; + app->log_in_with_credentials(credentials, + [&](std::shared_ptr user_arg, std::optional error) { + REQUIRE_FALSE(error); + REQUIRE(user_arg); + user = std::move(user_arg); + }); + REQUIRE(user); + return user; +} + +app::AppError failed_log_in(std::shared_ptr app, app::AppCredentials credentials) +{ + std::optional err; + app->log_in_with_credentials(credentials, + [&](std::shared_ptr user, std::optional error) { + REQUIRE(error); + REQUIRE_FALSE(user); + err = error; + }); + REQUIRE(err); + return *err; +} + #endif // REALM_ENABLE_AUTH_TESTS #if REALM_ENABLE_SYNC diff --git a/test/object-store/util/sync/sync_test_utils.hpp b/test/object-store/util/sync/sync_test_utils.hpp index b82eb07d18e..507c610e876 100644 --- a/test/object-store/util/sync/sync_test_utils.hpp +++ b/test/object-store/util/sync/sync_test_utils.hpp @@ -166,6 +166,12 @@ struct AutoVerifiedEmailCredentials : app::AppCredentials { AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app); +std::shared_ptr log_in(std::shared_ptr app, + app::AppCredentials credentials = app::AppCredentials::anonymous()); + +app::AppError failed_log_in(std::shared_ptr app, + app::AppCredentials credentials = app::AppCredentials::anonymous()); + #endif // REALM_ENABLE_AUTH_TESTS namespace reset_utils { diff --git a/test/object-store/util/test_file.cpp b/test/object-store/util/test_file.cpp index 11c9e678bc8..86ee114fa07 100644 --- a/test/object-store/util/test_file.cpp +++ b/test/object-store/util/test_file.cpp @@ -129,28 +129,82 @@ DBOptions InMemoryTestFile::options() const return options; } +OfflineAppSession::Config::Config(std::shared_ptr t) + : transport(t) +{ +} + +OfflineAppSession::OfflineAppSession(OfflineAppSession::Config config) + : m_transport(std::move(config.transport)) + , m_delete_storage(config.delete_storage) +{ + REALM_ASSERT(m_transport); + app::App::Config app_config; + set_app_config_defaults(app_config, m_transport); + + util::Logger::set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); + + if (config.storage_path) { + m_base_file_path = *config.storage_path; + } + else { + m_base_file_path = util::make_temp_dir(); + } + + util::try_make_dir(m_base_file_path); + app::BackingStoreConfig bsc; + bsc.base_file_path = m_base_file_path; + bsc.metadata_mode = app::BackingStoreConfig::MetadataMode::NoEncryption; + m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, bsc); +} + +OfflineAppSession::~OfflineAppSession() +{ + if (util::File::exists(m_base_file_path) && m_delete_storage) { + try { + m_app->backing_store()->reset_for_testing(); + util::try_remove_dir_recursive(m_base_file_path); + } + catch (const std::exception& ex) { + std::cerr << ex.what() << "\n"; + } + app::App::clear_cached_apps(); + } +} + + // MARK: - TestAppSession #if REALM_ENABLE_AUTH_TESTS -TestAppSession::TestAppSession() - : TestAppSession(get_runtime_app_session(), nullptr, DeleteApp{false}) +TestAppSession::Config::Config() + : Config(get_runtime_app_session(), nullptr, DeleteApp{false}) { } -TestAppSession::TestAppSession(AppSession session, +TestAppSession::Config::Config(AppSession session, std::shared_ptr custom_transport, DeleteApp delete_app #if REALM_ENABLE_SYNC , - ReconnectMode reconnect_mode, - std::shared_ptr custom_socket_provider + ReconnectMode mode, std::shared_ptr socket_provider #endif // REALM_SYNC ) - : m_app_session(std::make_unique(session)) - , m_base_file_path(util::make_temp_dir() + random_string(10)) - , m_delete_app(delete_app) - , m_transport(custom_transport) + : app_session(std::make_unique(std::move(session))) + , transport(std::move(custom_transport)) + , delete_when_done(delete_app) +#if REALM_ENABLE_SYNC + , reconnect_mode(std::move(mode)) + , custom_socket_provider(std::move(socket_provider)) +#endif // REALM_SYNC +{ +} + +TestAppSession::TestAppSession(Config config) + : m_app_session(std::move(config.app_session)) + , m_delete_app(config.delete_when_done) + , m_transport(std::move(config.transport)) + , m_delete_storage(config.delete_storage) { if (!m_transport) m_transport = instance_of; @@ -158,6 +212,13 @@ TestAppSession::TestAppSession(AppSession session, util::Logger::set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); set_app_config_defaults(app_config, m_transport); + if (config.storage_path) { + m_base_file_path = *config.storage_path; + } + else { + m_base_file_path = util::make_temp_dir(); + } + util::try_make_dir(m_base_file_path); app::BackingStoreConfig bsc; bsc.base_file_path = m_base_file_path; @@ -166,8 +227,8 @@ TestAppSession::TestAppSession(AppSession session, #if REALM_ENABLE_SYNC SyncClientConfig sc_config; sc_config.backing_store_config = bsc; - sc_config.reconnect_mode = reconnect_mode; - sc_config.socket_provider = custom_socket_provider; + sc_config.reconnect_mode = config.reconnect_mode; + sc_config.socket_provider = std::move(config.custom_socket_provider); // With multiplexing enabled, the linger time controls how long a // connection is kept open for reuse. In tests, we want to shut // down sync clients immediately. @@ -193,7 +254,9 @@ TestAppSession::~TestAppSession() #else m_app->backing_store()->reset_for_testing(); #endif - util::try_remove_dir_recursive(m_base_file_path); + if (m_delete_storage) { + util::try_remove_dir_recursive(m_base_file_path); + } } catch (const std::exception& ex) { std::cerr << ex.what() << "\n"; diff --git a/test/object-store/util/test_file.hpp b/test/object-store/util/test_file.hpp index a2a876981f7..afa87a34076 100644 --- a/test/object-store/util/test_file.hpp +++ b/test/object-store/util/test_file.hpp @@ -25,13 +25,15 @@ #include #include +#include +#include "test_utils.hpp" + #include #include #if REALM_ENABLE_SYNC #include #include -#include "test_utils.hpp" #include #include @@ -116,18 +118,111 @@ void on_change_but_no_notify(realm::Realm& realm); #endif // TEST_ENABLE_LOGGING #endif // TEST_LOGGING_LEVEL +static const std::string profile_0_name = "Ursus americanus Ursus boeckhi"; +static const std::string profile_0_first_name = "Ursus americanus"; +static const std::string profile_0_last_name = "Ursus boeckhi"; +static const std::string profile_0_email = "Ursus ursinus"; +static const std::string profile_0_picture_url = "Ursus malayanus"; +static const std::string profile_0_gender = "Ursus thibetanus"; +static const std::string profile_0_birthday = "Ursus americanus"; +static const std::string profile_0_min_age = "Ursus maritimus"; +static const std::string profile_0_max_age = "Ursus arctos"; + +static const nlohmann::json profile_0 = { + {"name", profile_0_name}, {"first_name", profile_0_first_name}, {"last_name", profile_0_last_name}, + {"email", profile_0_email}, {"picture_url", profile_0_picture_url}, {"gender", profile_0_gender}, + {"birthday", profile_0_birthday}, {"min_age", profile_0_min_age}, {"max_age", profile_0_max_age}}; + +static nlohmann::json user_json(std::string access_token, std::string user_id = realm::random_string(15)) +{ + return {{"access_token", access_token}, + {"refresh_token", access_token}, + {"user_id", user_id}, + {"device_id", "Panda Bear"}}; +} + +static nlohmann::json user_profile_json(std::string user_id = realm::random_string(15), + std::string identity_0_id = "Ursus arctos isabellinus", + std::string identity_1_id = "Ursus arctos horribilis", + std::string provider_type = "anon-user") +{ + return {{"user_id", user_id}, + {"identities", + {{{"id", identity_0_id}, {"provider_type", provider_type}}, + {{"id", identity_1_id}, {"provider_type", "lol_wut"}}}}, + {"data", profile_0}}; +} + +static const std::string good_access_token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2RldklkIjoi" + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU0M2Rk" + "Y2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s"; + +static const std::string good_access_token2 = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJleHAiOjE1ODkzMDE3MjAsImlhdCI6MTU4NDExODcyMCwiaXNzIjoiNWU2YmJiYzBhNmI3ZGZkM2UyNTA0OGI3Iiwic3RpdGNoX2RldklkIjoi" + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU2YmJi" + "YzBhNmI3ZGZkM2UyNTA0OGIzIiwidHlwIjoiYWNjZXNzIn0.eSX4QMjIOLbdOYOPzQrD_racwLUk1HGFgxtx2a34k80"; + +static const std::string bad_access_token = "lolwut"; +static const std::string dummy_device_id = "123400000000000000000000"; + +class OfflineAppSession { +public: + struct Config { + Config(std::shared_ptr); + std::shared_ptr transport; + bool delete_storage = true; + std::optional storage_path; + }; + OfflineAppSession(Config); + ~OfflineAppSession(); + + std::shared_ptr app() const noexcept + { + return m_app; + } + realm::app::GenericNetworkTransport* transport() + { + return m_transport.get(); + } + std::shared_ptr const& backing_store() const noexcept + { + return m_app->backing_store(); + } + +private: + std::shared_ptr m_app; + std::string m_base_file_path; + std::shared_ptr m_transport; + bool m_delete_storage = true; +}; + #if REALM_ENABLE_AUTH_TESTS using DeleteApp = realm::util::TaggedBool; class TestAppSession { public: - TestAppSession(); - TestAppSession(realm::AppSession, std::shared_ptr = nullptr, DeleteApp = true + struct Config { + Config(); + Config(realm::AppSession, std::shared_ptr = nullptr, DeleteApp = true #if REALM_ENABLE_SYNC - , - realm::ReconnectMode reconnect_mode = realm::ReconnectMode::normal, - std::shared_ptr custom_socket_provider = nullptr + , + realm::ReconnectMode mode = realm::ReconnectMode::normal, + std::shared_ptr socket_provider = nullptr +#endif // REALM_ENABLE_SYNC + ); + std::unique_ptr app_session; + std::shared_ptr transport; + DeleteApp delete_when_done; + bool delete_storage = true; + std::optional storage_path; +#if REALM_ENABLE_SYNC + realm::ReconnectMode reconnect_mode = realm::ReconnectMode::normal; + std::shared_ptr custom_socket_provider = nullptr; #endif // REALM_SYNC - ); + }; + TestAppSession(Config config = {}); ~TestAppSession(); std::shared_ptr app() const noexcept @@ -160,6 +255,7 @@ class TestAppSession { std::string m_base_file_path; bool m_delete_app = true; std::shared_ptr m_transport; + bool m_delete_storage = true; }; void set_app_config_defaults(realm::app::App::Config& app_config,