diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9e62cdace..2d80254703d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,24 @@ ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) -* None. +* Logging into a single user using multiple auth providers created a separate SyncUser per auth provider. This mostly worked, but had some quirks: + - Sync sessions would not necessarily be associated with the specific SyncUser used to create them. As a result, querying a user for its sessions could give incorrect results, and logging one user out could close the wrong sessions. + - Existing local synchronized Realm files created using version of Realm from August - November 2020 would sometimes not be opened correctly and would instead be redownloaded. + - Removing one of the SyncUsers would delete all local Realm files for all SyncUsers for that user. + - Deleting the server-side user via one of the SyncUsers left the other SyncUsers in an invalid state. + - A SyncUser which was originally created via anonymous login and then linked to an identity would still be treated as an anonymous users and removed entirely on logout. + ([PR #6837](https://github.com/realm/realm-core/pull/6837), since v10.0.0) +* Reading existing logged-in users on app startup from the sync metadata Realm performed three no-op writes per user on the metadata Realm ([PR #6837](https://github.com/realm/realm-core/pull/6837), since v10.0.0). +* If a user was logged out while an access token refresh was in progress, the refresh completing would mark the user as logged in again and the user would be in an inconsistent state ([PR #6837](https://github.com/realm/realm-core/pull/6837), since v10.0.0). ### Breaking changes -* None. +* SyncUser::provider_type() and realm_user_get_auth_provider() have been removed. Users don't have provider types; identities do. `SyncUser::is_anonymous()` is a more correct version of checking if the provider type is anonymous ([PR #6837](https://github.com/realm/realm-core/pull/6837)). +* SyncUser no longer has a `local_identity()`. `identity()` has been guaranteed to be unique per App ever since v10 ([PR #6837](https://github.com/realm/realm-core/pull/6837)). +* SyncUser no longer overrides operator==. Pointer equality should be used to compare sync users ([PR #6837](https://github.com/realm/realm-core/pull/6837)). ### Compatibility * Fileformat: Generates files with format v23. Reads and automatically upgrade from fileformat v5. +* The metadata Realm used to store sync users has had its schema version bumped. It is automatically migrated to the new version on first open. Downgrading to older version of Realm after upgrading will discard stored user tokens and require logging back in. ----------- diff --git a/src/realm.h b/src/realm.h index 5c68057c121..e05f99f3df0 100644 --- a/src/realm.h +++ b/src/realm.h @@ -3232,13 +3232,9 @@ RLM_API realm_user_state_e realm_user_get_state(const realm_user_t* user) RLM_AP RLM_API bool realm_user_get_all_identities(const realm_user_t* user, realm_user_identity_t* out_identities, size_t capacity, size_t* out_n); -RLM_API const char* realm_user_get_local_identity(const realm_user_t*) RLM_API_NOEXCEPT; - // returned pointer must be manually released with realm_free() RLM_API char* realm_user_get_device_id(const realm_user_t*) RLM_API_NOEXCEPT; -RLM_API realm_auth_provider_e realm_user_get_auth_provider(const realm_user_t*) RLM_API_NOEXCEPT; - /** * Log out the user and mark it as logged out. * diff --git a/src/realm/collection.hpp b/src/realm/collection.hpp index 297c8fc1a61..d0cde6961c7 100644 --- a/src/realm/collection.hpp +++ b/src/realm/collection.hpp @@ -19,7 +19,7 @@ struct CollectionIterator; /// Collections are bound to particular properties of an object. In a /// collection's public interface, the implementation must take care to keep the /// object consistent with the persisted state, mindful of the fact that the -/// state may have changed as a consquence of modifications from other instances +/// state may have changed as a consequence of modifications from other instances /// referencing the same persisted state. class CollectionBase { public: diff --git a/src/realm/object-store/c_api/app.cpp b/src/realm/object-store/c_api/app.cpp index 2c520ed46d3..6d0568a0d2f 100644 --- a/src/realm/object-store/c_api/app.cpp +++ b/src/realm/object-store/c_api/app.cpp @@ -642,11 +642,6 @@ RLM_API bool realm_user_get_all_identities(const realm_user_t* user, realm_user_ }); } -RLM_API const char* realm_user_get_local_identity(const realm_user_t* user) noexcept -{ - return (*user)->local_identity().c_str(); -} - RLM_API char* realm_user_get_device_id(const realm_user_t* user) noexcept { if ((*user)->has_device_id()) { @@ -656,11 +651,6 @@ RLM_API char* realm_user_get_device_id(const realm_user_t* user) noexcept return nullptr; } -RLM_API realm_auth_provider_e realm_user_get_auth_provider(const realm_user_t* user) noexcept -{ - return realm_auth_provider_e(enum_from_provider_type((*user)->provider_type())); -} - RLM_API bool realm_user_log_out(realm_user_t* user) { return wrap_err([&] { diff --git a/src/realm/object-store/c_api/types.hpp b/src/realm/object-store/c_api/types.hpp index 13b8f3ac18d..d12d7625296 100644 --- a/src/realm/object-store/c_api/types.hpp +++ b/src/realm/object-store/c_api/types.hpp @@ -661,7 +661,7 @@ struct realm_user : realm::c_api::WrapC, std::shared_ptr { bool equals(const WrapC& other) const noexcept final { if (auto ptr = dynamic_cast(&other)) { - return *get() == *(ptr->get()); + return get() == ptr->get(); } return false; } diff --git a/src/realm/object-store/impl/realm_coordinator.cpp b/src/realm/object-store/impl/realm_coordinator.cpp index f5a8e13c217..6cfa3858d2e 100644 --- a/src/realm/object-store/impl/realm_coordinator.cpp +++ b/src/realm/object-store/impl/realm_coordinator.cpp @@ -183,7 +183,7 @@ void RealmCoordinator::set_config(const Realm::Config& config) if (config.sync_config) { auto old_user = m_config.sync_config->user; auto new_user = config.sync_config->user; - if (old_user && new_user && *old_user != *new_user) { + if (old_user != new_user) { throw LogicError( ErrorCodes::MismatchedConfig, util::format("Realm at path '%1' already opened with different sync user.", config.path)); diff --git a/src/realm/object-store/sync/app.cpp b/src/realm/object-store/sync/app.cpp index 54da64a8674..fd6cddaf40c 100644 --- a/src/realm/object-store/sync/app.cpp +++ b/src/realm/object-store/sync/app.cpp @@ -595,9 +595,8 @@ void App::get_profile(const std::shared_ptr& sync_user, SyncUserIdentity(get(doc, "id"), get(doc, "provider_type"))); } - sync_user->update_identities(identities); - sync_user->update_user_profile(SyncUserProfile(get(profile_json, "data"))); - sync_user->set_state(SyncUser::State::LoggedIn); + sync_user->update_user_profile(std::move(identities), + SyncUserProfile(get(profile_json, "data"))); self->m_sync_manager->set_current_user(sync_user->identity()); self->emit_change_to_subscribers(*self); } @@ -644,7 +643,7 @@ void App::log_in_with_credentials( // is already an anonymous session active, reuse it if (credentials.provider() == AuthProvider::ANONYMOUS) { for (auto&& user : m_sync_manager->all_users()) { - if (user->provider_type() == credentials.provider_as_string() && user->is_logged_in()) { + if (user->is_anonymous()) { completion(switch_user(user), util::none); return; } @@ -686,8 +685,7 @@ void App::log_in_with_credentials( else { sync_user = self->m_sync_manager->get_user( get(json, "user_id"), get(json, "refresh_token"), - get(json, "access_token"), credentials.provider_as_string(), - get(json, "device_id")); + get(json, "access_token"), get(json, "device_id")); } } catch (const AppError& e) { @@ -758,11 +756,7 @@ std::shared_ptr App::switch_user(const std::shared_ptr& user if (!user || user->state() != SyncUser::State::LoggedIn) { throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out"); } - - auto users = m_sync_manager->all_users(); - auto it = std::find(users.begin(), users.end(), user); - - if (it == users.end()) { + if (!verify_user_present(user)) { throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist"); } diff --git a/src/realm/object-store/sync/impl/sync_file.cpp b/src/realm/object-store/sync/impl/sync_file.cpp index 6e6e075b7af..c59a2e2b321 100644 --- a/src/realm/object-store/sync/impl/sync_file.cpp +++ b/src/realm/object-store/sync/impl/sync_file.cpp @@ -295,11 +295,11 @@ bool SyncFileManager::copy_realm_file(const std::string& old_path, const std::st return true; } -bool SyncFileManager::remove_realm(const std::string& user_identity, const std::string& local_identity, +bool SyncFileManager::remove_realm(const std::string& user_identity, + const std::vector& legacy_user_identities, const std::string& raw_realm_path, const std::string& partition) const { - util::Optional existing = - get_existing_realm_file_path(user_identity, local_identity, raw_realm_path, partition); + auto existing = get_existing_realm_file_path(user_identity, legacy_user_identities, raw_realm_path, partition); if (existing) { return remove_realm(*existing); } @@ -327,10 +327,10 @@ static bool try_file_remove(const std::string& path) noexcept } } -util::Optional SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, - const std::string& local_user_identity, - const std::string& realm_file_name, - const std::string& partition) const +util::Optional +SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, + const std::vector& legacy_user_identities, + const std::string& realm_file_name, const std::string& partition) const { std::string preferred_name = preferred_realm_path_without_suffix(user_identity, realm_file_name); if (try_file_exists(preferred_name)) { @@ -365,14 +365,14 @@ util::Optional SyncFileManager::get_existing_realm_file_path(const } } - if (!local_user_identity.empty()) { + for (auto& legacy_identity : legacy_user_identities) { // retain support for legacy paths - std::string old_path = legacy_realm_file_path(local_user_identity, realm_file_name); + std::string old_path = legacy_realm_file_path(legacy_identity, realm_file_name); if (try_file_exists(old_path)) { return old_path; } // retain support for legacy local identity paths - std::string old_local_identity_path = legacy_local_identity_path(local_user_identity, partition); + std::string old_local_identity_path = legacy_local_identity_path(legacy_identity, partition); if (try_file_exists(old_local_identity_path)) { return old_local_identity_path; } @@ -381,11 +381,12 @@ util::Optional SyncFileManager::get_existing_realm_file_path(const return util::none; } -std::string SyncFileManager::realm_file_path(const std::string& user_identity, const std::string& local_user_identity, +std::string SyncFileManager::realm_file_path(const std::string& user_identity, + const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const { - util::Optional existing_path = - get_existing_realm_file_path(user_identity, local_user_identity, realm_file_name, partition); + auto existing_path = + get_existing_realm_file_path(user_identity, legacy_user_identities, realm_file_name, partition); if (existing_path) { return *existing_path; } diff --git a/src/realm/object-store/sync/impl/sync_file.hpp b/src/realm/object-store/sync/impl/sync_file.hpp index 455f03f640d..7750ae85748 100644 --- a/src/realm/object-store/sync/impl/sync_file.hpp +++ b/src/realm/object-store/sync/impl/sync_file.hpp @@ -69,15 +69,16 @@ class SyncFileManager { static bool try_file_exists(const std::string& path) noexcept; util::Optional get_existing_realm_file_path(const std::string& user_identity, - const std::string& local_user_identity, + const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; /// Return the path for a given Realm, creating the user directory if it does not already exist. - std::string realm_file_path(const std::string& user_identity, const std::string& local_user_identity, + std::string realm_file_path(const std::string& user_identity, + const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; /// Remove the Realm at a given path for a given user. Returns `true` if the remove operation fully succeeds. - bool remove_realm(const std::string& user_identity, const std::string& local_identity, + bool remove_realm(const std::string& user_identity, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; /// Remove the Realm whose primary Realm file is located at `absolute_path`. Returns `true` if the remove diff --git a/src/realm/object-store/sync/impl/sync_metadata.cpp b/src/realm/object-store/sync/impl/sync_metadata.cpp index bb84e02fce6..8fc53c30da6 100644 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ b/src/realm/object-store/sync/impl/sync_metadata.cpp @@ -34,6 +34,8 @@ #include #include +using namespace realm; + namespace { static const char* const c_sync_userMetadata = "UserMetadata"; static const char* const c_sync_identityMetadata = "UserIdentity"; @@ -41,10 +43,9 @@ static const char* const c_sync_app_metadata = "AppMetadata"; static const char* const c_sync_current_user_identity = "current_user_identity"; -/* User keys*/ -static const char* const c_sync_marked_for_removal = "marked_for_removal"; +/* User keys */ static const char* const c_sync_identity = "identity"; -static const char* const c_sync_local_uuid = "local_uuid"; +static const char* const c_sync_legacy_uuids = "legacy_uuids"; static const char* const c_sync_refresh_token = "refresh_token"; static const char* const c_sync_access_token = "access_token"; static const char* const c_sync_identities = "identities"; @@ -61,7 +62,7 @@ static const char* const c_sync_fileActionMetadata = "FileActionMetadata"; static const char* const c_sync_original_name = "original_name"; static const char* const c_sync_new_name = "new_name"; static const char* const c_sync_action = "action"; -static const char* const c_sync_url = "url"; +static const char* const c_sync_partition = "url"; static const char* const c_sync_app_metadata_id = "id"; static const char* const c_sync_app_metadata_deployment_model = "deployment_model"; @@ -72,47 +73,148 @@ static const char* const c_sync_app_metadata_ws_hostname = "ws_hostname"; realm::Schema make_schema() { using namespace realm; - return Schema{{c_sync_identityMetadata, - {{c_sync_user_id, PropertyType::String}, {c_sync_provider_type, PropertyType::String}}}, - {c_sync_userMetadata, - {{c_sync_identity, PropertyType::String}, - {c_sync_local_uuid, PropertyType::String}, - {c_sync_marked_for_removal, PropertyType::Bool}, - {c_sync_refresh_token, PropertyType::String | PropertyType::Nullable}, - {c_sync_provider_type, PropertyType::String}, - {c_sync_access_token, PropertyType::String | PropertyType::Nullable}, - {c_sync_identities, PropertyType::Object | PropertyType::Array, c_sync_identityMetadata}, - {c_sync_state, PropertyType::Int}, - {c_sync_device_id, PropertyType::String}, - {c_sync_profile_data, PropertyType::String}, - {c_sync_local_realm_paths, PropertyType::Set | PropertyType::String}}}, - {c_sync_fileActionMetadata, - { - {c_sync_original_name, PropertyType::String, Property::IsPrimary{true}}, - {c_sync_new_name, PropertyType::String | PropertyType::Nullable}, - {c_sync_action, PropertyType::Int}, - {c_sync_url, PropertyType::String}, - {c_sync_identity, PropertyType::String}, - }}, - {c_sync_current_user_identity, {{c_sync_current_user_identity, PropertyType::String}}}, - {c_sync_app_metadata, - {{c_sync_app_metadata_id, PropertyType::Int, Property::IsPrimary{true}}, - {c_sync_app_metadata_deployment_model, PropertyType::String}, - {c_sync_app_metadata_location, PropertyType::String}, - {c_sync_app_metadata_hostname, PropertyType::String}, - {c_sync_app_metadata_ws_hostname, PropertyType::String}}}}; + return Schema{ + {c_sync_identityMetadata, + ObjectSchema::ObjectType::Embedded, + { + {c_sync_user_id, PropertyType::String}, + {c_sync_provider_type, PropertyType::String}, + }}, + {c_sync_userMetadata, + {{c_sync_identity, PropertyType::String}, + {c_sync_legacy_uuids, PropertyType::String | PropertyType::Array}, + {c_sync_refresh_token, PropertyType::String | PropertyType::Nullable}, + {c_sync_access_token, PropertyType::String | PropertyType::Nullable}, + {c_sync_identities, PropertyType::Object | PropertyType::Array, c_sync_identityMetadata}, + {c_sync_state, PropertyType::Int}, + {c_sync_device_id, PropertyType::String}, + {c_sync_profile_data, PropertyType::String}, + {c_sync_local_realm_paths, PropertyType::Set | PropertyType::String}}}, + {c_sync_fileActionMetadata, + { + {c_sync_original_name, PropertyType::String, Property::IsPrimary{true}}, + {c_sync_new_name, PropertyType::String | PropertyType::Nullable}, + {c_sync_action, PropertyType::Int}, + {c_sync_partition, PropertyType::String}, + {c_sync_identity, PropertyType::String}, + }}, + {c_sync_current_user_identity, + { + {c_sync_current_user_identity, PropertyType::String}, + }}, + {c_sync_app_metadata, + { + {c_sync_app_metadata_id, PropertyType::Int, Property::IsPrimary{true}}, + {c_sync_app_metadata_deployment_model, PropertyType::String}, + {c_sync_app_metadata_location, PropertyType::String}, + {c_sync_app_metadata_hostname, PropertyType::String}, + {c_sync_app_metadata_ws_hostname, PropertyType::String}, + }}, + }; } -} // anonymous namespace +void migrate_to_v7(std::shared_ptr old_realm, std::shared_ptr realm) +{ + // Before schema version 7 there may have been multiple UserMetadata entries + // for a single user_id with different provider types, so we need to merge + // any duplicates together -namespace realm { + TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); + TableRef old_table = ObjectStore::table_for_object_type(old_realm->read_group(), c_sync_userMetadata); + if (table->is_empty()) + return; + REALM_ASSERT(table->size() == old_table->size()); + + ColKey id_col = table->get_column_key(c_sync_identity); + ColKey old_uuid_col = old_table->get_column_key("local_uuid"); + ColKey new_uuid_col = table->get_column_key(c_sync_legacy_uuids); + ColKey state_col = table->get_column_key(c_sync_state); + + std::unordered_map users; + for (size_t i = 0, j = 0; i < table->size(); ++j) { + auto obj = table->get_object(i); + + // Move the local uuid from the old column to the list + auto old_obj = old_table->get_object(j); + obj.get_list(new_uuid_col).add(old_obj.get(old_uuid_col)); + + // Check if we've already seen an object with the same id. If not, store + // this one and move on + std::string user_id = obj.get(id_col); + auto& existing = users[obj.get(id_col)]; + if (!existing.is_valid()) { + existing = obj; + ++i; + continue; + } + + // We have a second object for the same id, so we need to merge them. + // First we merge the state: if one is logged in and the other isn't, + // we'll use the logged-in state and tokens. If both are logged in, we'll + // use the more recent login. If one is logged out and the other is + // removed we'll use the logged out state. If both are logged out or + // both are removed then it doesn't matter which we pick. + using State = SyncUser::State; + auto state = State(obj.get(state_col)); + auto existing_state = State(existing.get(state_col)); + if (state == existing_state) { + if (state == State::LoggedIn) { + RealmJWT token_1(existing.get(c_sync_access_token)); + RealmJWT token_2(obj.get(c_sync_access_token)); + if (token_1.issued_at < token_2.issued_at) { + existing.set(c_sync_refresh_token, obj.get(c_sync_refresh_token)); + existing.set(c_sync_access_token, obj.get(c_sync_access_token)); + } + } + } + else if (state == State::LoggedIn || existing_state == State::Removed) { + existing.set(c_sync_state, int64_t(state)); + existing.set(c_sync_refresh_token, obj.get(c_sync_refresh_token)); + existing.set(c_sync_access_token, obj.get(c_sync_access_token)); + } + + // Next we merge the list properties (identities, legacy uuids, realm file paths) + { + auto dest = existing.get_linklist(c_sync_identities); + auto src = obj.get_linklist(c_sync_identities); + for (size_t i = 0, size = src.size(); i < size; ++i) { + if (dest.find_first(src.get(i)) == npos) { + dest.add(src.get(i)); + } + } + } + { + auto dest = existing.get_list(c_sync_legacy_uuids); + auto src = obj.get_list(c_sync_legacy_uuids); + for (size_t i = 0, size = src.size(); i < size; ++i) { + if (dest.find_first(src.get(i)) == npos) { + dest.add(src.get(i)); + } + } + } + { + auto dest = existing.get_set(c_sync_local_realm_paths); + auto src = obj.get_set(c_sync_local_realm_paths); + for (size_t i = 0, size = src.size(); i < size; ++i) { + dest.insert(src.get(i)); + } + } + + + // Finally we delete the duplicate object. We don't increment `i` as it's + // now the index of the object just after the one we're deleting. + obj.remove(); + } +} + +} // anonymous namespace // MARK: - Sync metadata manager SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, util::Optional> encryption_key) { - constexpr uint64_t SCHEMA_VERSION = 6; + constexpr uint64_t SCHEMA_VERSION = 7; if (!REALM_PLATFORM_APPLE && should_encrypt && !encryption_key) throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); @@ -125,6 +227,13 @@ SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, m_metadata_config.scheduler = util::Scheduler::make_dummy(); if (encryption_key) m_metadata_config.encryption_key = std::move(*encryption_key); + m_metadata_config.automatically_handle_backlinks_in_migrations = true; + m_metadata_config.migration_function = [](std::shared_ptr old_realm, std::shared_ptr realm, + Schema&) { + if (old_realm->schema_version() < 7) { + migrate_to_v7(old_realm, realm); + } + }; auto realm = open_realm(should_encrypt, encryption_key != none); @@ -135,8 +244,7 @@ SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, object_schema->persisted_properties[2].column_key, object_schema->persisted_properties[3].column_key, object_schema->persisted_properties[4].column_key, object_schema->persisted_properties[5].column_key, object_schema->persisted_properties[6].column_key, object_schema->persisted_properties[7].column_key, - object_schema->persisted_properties[8].column_key, object_schema->persisted_properties[9].column_key, - object_schema->persisted_properties[10].column_key}; + object_schema->persisted_properties[8].column_key}; object_schema = realm->schema().find(c_sync_fileActionMetadata); m_file_action_schema = { @@ -145,8 +253,6 @@ SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, object_schema->persisted_properties[4].column_key, }; - object_schema = realm->schema().find(c_sync_current_user_identity); - object_schema = realm->schema().find(c_sync_app_metadata); m_app_metadata_schema = { object_schema->persisted_properties[0].column_key, object_schema->persisted_properties[1].column_key, @@ -168,7 +274,13 @@ SyncUserMetadataResults SyncMetadataManager::get_users(bool marked) const { auto realm = get_realm(); TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - Query query = table->where().equal(m_user_schema.marked_for_removal_col, marked); + Query query; + if (marked) { + query = table->where().equal(m_user_schema.state_col, int64_t(SyncUser::State::Removed)); + } + else { + query = table->where().not_equal(m_user_schema.state_col, int64_t(SyncUser::State::Removed)); + } return SyncUserMetadataResults(Results(realm, std::move(query)), m_user_schema); } @@ -213,7 +325,6 @@ void SyncMetadataManager::set_current_user_identity(const std::string& identity) } util::Optional SyncMetadataManager::get_or_make_user_metadata(const std::string& identity, - const std::string& provider_type, bool make_if_absent) const { auto realm = get_realm(); @@ -221,79 +332,54 @@ util::Optional SyncMetadataManager::get_or_make_user_metadata( // Retrieve or create the row for this object. TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - Query query = table->where() - .equal(schema.identity_col, StringData(identity)) - .equal(schema.provider_type_col, StringData(provider_type)); + Query query = table->where().equal(schema.identity_col, StringData(identity)); Results results(realm, std::move(query)); REALM_ASSERT_DEBUG(results.size() < 2); - auto row = results.first(); + auto obj = results.first(); - if (!row) { + if (!obj) { if (!make_if_absent) return none; realm->begin_transaction(); // Check the results again. - row = results.first(); - if (!row) { - // Because "making this user" is our last action, set this new user as the current user - TableRef currentUserIdentityTable = - ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - Obj currentUserIdentityObj; - if (currentUserIdentityTable->is_empty()) - currentUserIdentityObj = currentUserIdentityTable->create_object(); - else - currentUserIdentityObj = *currentUserIdentityTable->begin(); - - auto obj = table->create_object(); - - currentUserIdentityObj.set(c_sync_current_user_identity, identity); - - std::string uuid = util::uuid_string(); - obj.set(schema.identity_col, identity); - obj.set(schema.provider_type_col, provider_type); - obj.set(schema.local_uuid_col, uuid); - obj.set(schema.marked_for_removal_col, false); - obj.set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); - realm->commit_transaction(); - return SyncUserMetadata(schema, std::move(realm), std::move(obj)); - } - else { - // Someone beat us to adding this user. - if (row->get(schema.marked_for_removal_col)) { - // User is dead. Revive or return none. - if (make_if_absent) { - row->set(schema.marked_for_removal_col, false); - realm->commit_transaction(); - } - else { - realm->cancel_transaction(); - return none; - } - } - else { - // User is alive, nothing else to do. - realm->cancel_transaction(); - } - return SyncUserMetadata(schema, std::move(realm), std::move(*row)); - } + obj = results.first(); + } + if (!obj) { + // Because "making this user" is our last action, set this new user as the current user + TableRef currentUserIdentityTable = + ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); + + Obj currentUserIdentityObj; + if (currentUserIdentityTable->is_empty()) + currentUserIdentityObj = currentUserIdentityTable->create_object(); + else + currentUserIdentityObj = *currentUserIdentityTable->begin(); + + obj = table->create_object(); + + currentUserIdentityObj.set(c_sync_current_user_identity, identity); + + obj->set(schema.identity_col, identity); + obj->set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); + realm->commit_transaction(); + return SyncUserMetadata(schema, std::move(realm), *obj); } // Got an existing user. - if (row->get(schema.marked_for_removal_col)) { + if (obj->get(schema.state_col) == int64_t(SyncUser::State::Removed)) { // User is dead. Revive or return none. - if (make_if_absent) { - realm->begin_transaction(); - row->set(schema.marked_for_removal_col, false); - realm->commit_transaction(); - } - else { + if (!make_if_absent) { return none; } + + if (!realm->is_in_transaction()) + realm->begin_transaction(); + obj->set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); + realm->commit_transaction(); } - return SyncUserMetadata(schema, std::move(realm), std::move(*row)); + return SyncUserMetadata(schema, std::move(realm), std::move(*obj)); } void SyncMetadataManager::make_file_action_metadata(StringData original_name, StringData partition_key_value, @@ -318,7 +404,7 @@ void SyncMetadataManager::make_file_action_metadata(StringData original_name, St obj.set(schema.idx_new_name, new_name); obj.set(schema.idx_action, static_cast(action)); - obj.set(schema.idx_url, partition_key_value); + obj.set(schema.idx_partition, partition_key_value); obj.set(schema.idx_user_identity, local_uuid); transaction.commit(); } @@ -478,11 +564,16 @@ SyncUser::State SyncUserMetadata::state() const return SyncUser::State(m_obj.get(m_schema.state_col)); } -std::string SyncUserMetadata::local_uuid() const +std::vector SyncUserMetadata::legacy_identities() const { REALM_ASSERT(m_realm); m_realm->refresh(); - return m_obj.get(m_schema.local_uuid_col); + std::vector uuids; + auto list = m_obj.get_list(m_schema.legacy_uuids_col); + for (size_t i = 0, size = list.size(); i < size; ++i) { + uuids.push_back(list.get(i)); + } + return uuids; } std::string SyncUserMetadata::refresh_token() const @@ -520,19 +611,22 @@ std::vector SyncUserMetadata::identities() const std::vector identities; for (size_t i = 0; i < linklist.size(); i++) { - auto obj_key = linklist.get(i); - auto obj = linklist.get_target_table()->get_object(obj_key); + auto obj = linklist.get_object(i); identities.push_back(user_identity_from_obj(obj)); } return identities; } -std::string SyncUserMetadata::provider_type() const +SyncUserProfile SyncUserMetadata::profile() const { REALM_ASSERT(m_realm); m_realm->refresh(); - return m_obj.get(m_schema.provider_type_col); + StringData result = m_obj.get(m_schema.profile_dump_col); + if (result.size() == 0) { + return SyncUserProfile(); + } + return SyncUserProfile(static_cast(bson::parse(std::string_view(result)))); } void SyncUserMetadata::set_refresh_token(const std::string& refresh_token) @@ -586,17 +680,9 @@ void SyncUserMetadata::set_identities(std::vector identities) link_list.clear(); for (auto& ident : identities) { - ObjKey obj_key = identities_table->where() - .equal(col_user_id, StringData(ident.id)) - .equal(col_provider_type, StringData(ident.provider_type)) - .find(); - if (!obj_key) { - auto obj = link_list.get_target_table()->create_object(); - obj.set(c_sync_user_id, ident.id); - obj.set(c_sync_provider_type, ident.provider_type); - obj_key = obj.get_key(); - } - link_list.add(obj_key); + auto obj = link_list.create_and_insert_linked_object(link_list.size()); + obj.set(col_user_id, ident.id); + obj.set(col_provider_type, ident.provider_type); } m_realm->commit_transaction(); @@ -624,15 +710,14 @@ void SyncUserMetadata::set_device_id(const std::string& device_id) m_realm->commit_transaction(); } -SyncUserProfile SyncUserMetadata::profile() const +void SyncUserMetadata::set_legacy_identities(const std::vector& uuids) { - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.profile_dump_col); - if (result.size() == 0) { - return SyncUserProfile(); - } - return SyncUserProfile(static_cast(bson::parse(std::string_view(result)))); + m_realm->begin_transaction(); + auto list = m_obj.get_list(m_schema.legacy_uuids_col); + list.clear(); + for (auto& uuid : uuids) + list.add(uuid); + m_realm->commit_transaction(); } void SyncUserMetadata::set_user_profile(const SyncUserProfile& profile) @@ -671,16 +756,6 @@ void SyncUserMetadata::add_realm_file_path(const std::string& path) m_realm->commit_transaction(); } -void SyncUserMetadata::mark_for_removal() -{ - if (m_invalid) - return; - - m_realm->begin_transaction(); - m_obj.set(m_schema.marked_for_removal_col, true); - m_realm->commit_transaction(); -} - void SyncUserMetadata::remove() { m_invalid = true; @@ -728,11 +803,11 @@ SyncFileActionMetadata::Action SyncFileActionMetadata::action() const return static_cast(m_obj.get(m_schema.idx_action)); } -std::string SyncFileActionMetadata::url() const +std::string SyncFileActionMetadata::partition() const { REALM_ASSERT(m_realm); m_realm->refresh(); - return m_obj.get(m_schema.idx_url); + return m_obj.get(m_schema.idx_partition); } void SyncFileActionMetadata::remove() @@ -751,5 +826,3 @@ void SyncFileActionMetadata::set_action(Action new_action) m_obj.set(m_schema.idx_action, static_cast(new_action)); m_realm->commit_transaction(); } - -} // namespace realm diff --git a/src/realm/object-store/sync/impl/sync_metadata.hpp b/src/realm/object-store/sync/impl/sync_metadata.hpp index bada9c22c3a..2ab0d84b3cf 100644 --- a/src/realm/object-store/sync/impl/sync_metadata.hpp +++ b/src/realm/object-store/sync/impl/sync_metadata.hpp @@ -52,16 +52,14 @@ class SyncAppMetadata { class SyncUserMetadata { public: struct Schema { - // The ROS identity of the user. This, plus the auth server URL, uniquely identifies a user. + // The server-supplied user_id for the user. Unique per App. ColKey identity_col; - // A locally issued UUID for the user. This is used to generate the on-disk user directory. - ColKey local_uuid_col; - // Whether or not this user has been marked for removal. - ColKey marked_for_removal_col; + // Locally generated UUIDs for the user. These are tracked to be able + // to open pre-existing Realm files, but are no longer generated or + // used for anything else. + ColKey legacy_uuids_col; // The cached refresh token for this user. ColKey refresh_token_col; - // The URL of the authentication server this user resides upon. - ColKey provider_type_col; // The cached access token for this user. ColKey access_token_col; // The identities for this user. @@ -79,8 +77,9 @@ class SyncUserMetadata { // Cannot be set after creation. std::string identity() const; - // Cannot be set after creation. - std::string local_uuid() const; + std::vector legacy_identities() const; + // for testing purposes only + void set_legacy_identities(const std::vector&); std::vector identities() const; void set_identities(std::vector); @@ -107,13 +106,6 @@ class SyncUserMetadata { SyncUser::State state() const; - // Cannot be set after creation. - std::string provider_type() const; - - // Mark the user as "ready for removal". Since Realm files cannot be safely deleted after being opened, the actual - // deletion of a user must be deferred until the next time the host application is launched. - void mark_for_removal(); - void remove(); bool is_valid() const @@ -141,8 +133,8 @@ class SyncFileActionMetadata { ColKey idx_new_name; // An enum describing the action to take. ColKey idx_action; - // The full remote URL of the Realm on the ROS. - ColKey idx_url; + // The partition key of the Realm. + ColKey idx_partition; // The local UUID of the user to whom the file action applies (despite the internal column name). ColKey idx_user_identity; }; @@ -167,7 +159,7 @@ class SyncFileActionMetadata { std::string user_local_uuid() const; Action action() const; - std::string url() const; + std::string partition() const; void remove(); void set_action(Action new_action); @@ -229,7 +221,6 @@ class SyncMetadataManager { // Retrieve or create user metadata. // Note: if `make_is_absent` is true and the user has been marked for deletion, it will be unmarked. util::Optional get_or_make_user_metadata(const std::string& identity, - const std::string& provider_type, bool make_if_absent = true) const; // Retrieve file action metadata. diff --git a/src/realm/object-store/sync/sync_manager.cpp b/src/realm/object-store/sync/sync_manager.cpp index 39bcae9e369..eba75289246 100644 --- a/src/realm/object-store/sync/sync_manager.cpp +++ b/src/realm/object-store/sync/sync_manager.cpp @@ -46,17 +46,7 @@ SyncManager::SyncManager() = default; void SyncManager::configure(std::shared_ptr app, const std::string& sync_route, const SyncClientConfig& config) { - struct UserCreationData { - std::string identity; - std::string refresh_token; - std::string access_token; - std::string provider_type; - std::vector identities; - SyncUser::State state; - std::string device_id; - }; - - std::vector users_to_add; + std::vector> users_to_add; { // Locking the mutex here ensures that it is released before locking m_user_mutex util::CheckedLockGuard lock(m_mutex); @@ -114,11 +104,8 @@ void SyncManager::configure(std::shared_ptr app, const std::string& sy auto user_data = users.get(i); auto refresh_token = user_data.refresh_token(); auto access_token = user_data.access_token(); - auto device_id = user_data.device_id(); if (!refresh_token.empty() && !access_token.empty()) { - users_to_add.push_back(UserCreationData{user_data.identity(), std::move(refresh_token), - std::move(access_token), user_data.provider_type(), - user_data.identities(), user_data.state(), device_id}); + users_to_add.push_back(std::make_shared(user_data, this)); } } @@ -145,15 +132,7 @@ void SyncManager::configure(std::shared_ptr app, const std::string& sy } { util::CheckedLockGuard lock(m_user_mutex); - for (auto& user_data : users_to_add) { - auto& identity = user_data.identity; - auto& provider_type = user_data.provider_type; - auto user = - std::make_shared(user_data.refresh_token, identity, provider_type, user_data.access_token, - user_data.state, user_data.device_id, this); - user->update_identities(user_data.identities); - m_users.emplace_back(std::move(user)); - } + m_users.insert(m_users.end(), users_to_add.begin(), users_to_add.end()); } } @@ -354,20 +333,16 @@ bool SyncManager::perform_metadata_update(util::FunctionRef SyncManager::get_user(const std::string& user_id, std::string refresh_token, - std::string access_token, const std::string provider_type, - std::string device_id) +std::shared_ptr SyncManager::get_user(const std::string& user_id, const std::string& refresh_token, + const std::string& access_token, const std::string& device_id) { util::CheckedLockGuard lock(m_user_mutex); - auto it = std::find_if(m_users.begin(), m_users.end(), [user_id, provider_type](const auto& user) { - return user->identity() == user_id && user->provider_type() == provider_type && - user->state() != SyncUser::State::Removed; + auto it = std::find_if(m_users.begin(), m_users.end(), [&](const auto& user) { + return user->identity() == user_id && user->state() != SyncUser::State::Removed; }); if (it == m_users.end()) { // No existing user. - auto new_user = - std::make_shared(std::move(refresh_token), user_id, provider_type, std::move(access_token), - SyncUser::State::LoggedIn, device_id, this); + auto new_user = std::make_shared(refresh_token, user_id, access_token, device_id, this); m_users.emplace(m_users.begin(), new_user); { util::CheckedLockGuard lock(m_file_system_mutex); @@ -380,7 +355,7 @@ std::shared_ptr SyncManager::get_user(const std::string& user_id, std: else { // LoggedOut => LoggedIn auto user = *it; REALM_ASSERT(user->state() != SyncUser::State::Removed); - user->update_state_and_tokens(SyncUser::State::LoggedIn, std::move(access_token), std::move(refresh_token)); + user->log_in(access_token, refresh_token); return user; } } @@ -423,39 +398,36 @@ std::shared_ptr SyncManager::get_current_user() const return cur_user_ident ? get_user_for_identity(*cur_user_ident) : nullptr; } -void SyncManager::log_out_user(const std::string& user_id) +void SyncManager::log_out_user(const SyncUser& user) { util::CheckedLockGuard lock(m_user_mutex); // Move this user to the end of the vector - if (m_users.size() > 1) { - auto it = std::find_if(m_users.begin(), m_users.end(), [user_id](const auto& user) { - return user->identity() == user_id; - }); + auto user_pos = std::partition(m_users.begin(), m_users.end(), [&](auto& u) { + return u.get() != &user; + }); - if (it != m_users.end()) - std::rotate(it, it + 1, m_users.end()); - } + auto active_user = std::find_if(m_users.begin(), user_pos, [](auto& u) { + return u->state() == SyncUser::State::LoggedIn; + }); util::CheckedLockGuard fs_lock(m_file_system_mutex); - bool was_active = (m_current_user && m_current_user->identity() == user_id) || - (m_metadata_manager && m_metadata_manager->get_current_user_identity() == user_id); + bool was_active = m_current_user.get() == &user || + (m_metadata_manager && m_metadata_manager->get_current_user_identity() == user.identity()); if (!was_active) return; // Set the current active user to the next logged in user, or null if none - for (auto& user : m_users) { - if (user->state() == SyncUser::State::LoggedIn) { - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity(user->identity()); - m_current_user = user; - return; - } + if (active_user != user_pos) { + m_current_user = *active_user; + if (m_metadata_manager) + m_metadata_manager->set_current_user_identity((*active_user)->identity()); + } + else { + m_current_user = nullptr; + if (m_metadata_manager) + m_metadata_manager->set_current_user_identity(""); } - - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity(""); - m_current_user = nullptr; } void SyncManager::set_current_user(const std::string& user_id) @@ -471,27 +443,14 @@ void SyncManager::set_current_user(const std::string& user_id) void SyncManager::remove_user(const std::string& user_id) { util::CheckedLockGuard lock(m_user_mutex); - auto user = get_user_for_identity(user_id); - if (!user) - return; - user->set_state(SyncUser::State::Removed); - - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (!m_metadata_manager) - return; - - for (size_t i = 0; i < m_metadata_manager->all_unmarked_users().size(); i++) { - auto metadata = m_metadata_manager->all_unmarked_users().get(i); - if (user->identity() == metadata.identity()) { - metadata.mark_for_removal(); - } - } + if (auto user = get_user_for_identity(user_id)) + user->invalidate(); } void SyncManager::delete_user(const std::string& user_id) { util::CheckedLockGuard lock(m_user_mutex); - // Avoid itterating over m_users twice by not calling `get_user_for_identity`. + // Avoid iterating over m_users twice by not calling `get_user_for_identity`. auto it = std::find_if(m_users.begin(), m_users.end(), [&user_id](auto& user) { return user->identity() == user_id; }); @@ -611,12 +570,12 @@ std::string SyncManager::path_for_realm(const SyncConfig& config, util::Optional } return string_from_partition(config.partition_value); }(); - path = m_file_manager->realm_file_path(user->identity(), user->local_identity(), file_name, + path = m_file_manager->realm_file_path(user->identity(), user->legacy_identities(), file_name, config.partition_value); } // Report the use of a Realm for this user, so the metadata can track it for clean up. perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(user->identity(), user->provider_type()); + auto metadata = manager.get_or_make_user_metadata(user->identity()); metadata->add_realm_file_path(path); }); return path; diff --git a/src/realm/object-store/sync/sync_manager.hpp b/src/realm/object-store/sync/sync_manager.hpp index 1f8a29f3f05..19233ddafce 100644 --- a/src/realm/object-store/sync/sync_manager.hpp +++ b/src/realm/object-store/sync/sync_manager.hpp @@ -180,8 +180,8 @@ class SyncManager : public std::enable_shared_from_this { // Get a sync user for a given identity, or create one if none exists yet, and set its token. // If a logged-out user exists, it will marked as logged back in. - std::shared_ptr get_user(const std::string& id, std::string refresh_token, std::string access_token, - const std::string provider_type, std::string device_id) + std::shared_ptr get_user(const std::string& user_id, const std::string& refresh_token, + const std::string& access_token, const std::string& device_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); // Get an existing user for a given identifier, if one exists and is logged in. @@ -194,7 +194,7 @@ class SyncManager : public std::enable_shared_from_this { std::shared_ptr get_current_user() const REQUIRES(!m_user_mutex, !m_file_system_mutex); // Log out a given user - void log_out_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); + void log_out_user(const SyncUser& user) REQUIRES(!m_user_mutex, !m_file_system_mutex); // Sets the currently active user. void set_current_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index 185b0e3a58e..9445af465f5 100644 --- a/src/realm/object-store/sync/sync_session.cpp +++ b/src/realm/object-store/sync/sync_session.cpp @@ -740,7 +740,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) return; } - // Dont't bother invoking m_config.error_handler if the sync is inactive. + // Don't bother invoking m_config.error_handler if the sync is inactive. // It does not make sense to call the handler when the session is closed. if (m_state == State::Inactive || m_state == State::Paused) { return; diff --git a/src/realm/object-store/sync/sync_user.cpp b/src/realm/object-store/sync/sync_user.cpp index 515b8341aa7..1f113287856 100644 --- a/src/realm/object-store/sync/sync_user.cpp +++ b/src/realm/object-store/sync/sync_user.cpp @@ -84,43 +84,68 @@ SyncUserIdentity::SyncUserIdentity(const std::string& id, const std::string& pro SyncUserContextFactory SyncUser::s_binding_context_factory; std::mutex SyncUser::s_binding_context_factory_mutex; -SyncUser::SyncUser(std::string refresh_token, const std::string identity, const std::string provider_type, - std::string access_token, SyncUser::State state, const std::string device_id, - SyncManager* sync_manager) - : m_provider_type(provider_type) - , m_identity(std::move(identity)) - , m_refresh_token(RealmJWT(std::move(refresh_token))) - , m_access_token(RealmJWT(std::move(access_token))) +SyncUser::SyncUser(const std::string& refresh_token, const std::string& id, const std::string& access_token, + const std::string& device_id, SyncManager* sync_manager) + : m_state(State::LoggedIn) + , m_identity(id) + , m_refresh_token(RealmJWT(refresh_token)) + , m_access_token(RealmJWT(access_token)) , m_device_id(device_id) , m_sync_manager(sync_manager) { - m_state.store(state); + REALM_ASSERT(!access_token.empty() && !refresh_token.empty()); { - std::lock_guard lock(s_binding_context_factory_mutex); + std::lock_guard lock(s_binding_context_factory_mutex); if (s_binding_context_factory) { m_binding_context = s_binding_context_factory(); } } - bool updated = m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); - metadata->set_state_and_tokens(state, m_access_token.token, m_refresh_token.token); + m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { + auto metadata = manager.get_or_make_user_metadata(m_identity); + metadata->set_state_and_tokens(State::LoggedIn, m_access_token.token, m_refresh_token.token); metadata->set_device_id(m_device_id); - m_local_identity = metadata->local_uuid(); + m_legacy_identities = metadata->legacy_identities(); this->m_user_profile = metadata->profile(); }); - if (!updated) - m_local_identity = m_identity; } -SyncUser::~SyncUser() {} +SyncUser::SyncUser(const SyncUserMetadata& data, SyncManager* sync_manager) + : m_state(data.state()) + , m_legacy_identities(data.legacy_identities()) + , m_identity(data.identity()) + , m_refresh_token(RealmJWT(data.refresh_token())) + , m_access_token(RealmJWT(data.access_token())) + , m_user_identities(data.identities()) + , m_user_profile(data.profile()) + , m_device_id(data.device_id()) + , m_sync_manager(sync_manager) +{ + // Check for inconsistent state in the metadata Realm. This shouldn't happen, + // but previous versions could sometimes mark a user as logged in with an + // empty refresh token. + if (m_state == State::LoggedIn && (m_refresh_token.token.empty() || m_access_token.token.empty())) { + m_state = State::LoggedOut; + m_refresh_token = {}; + m_access_token = {}; + } + + { + std::lock_guard lock(s_binding_context_factory_mutex); + if (s_binding_context_factory) { + m_binding_context = s_binding_context_factory(); + } + } +} std::shared_ptr SyncUser::sync_manager() const { util::CheckedLockGuard lk(m_mutex); if (m_state == State::Removed) { - throw std::logic_error(util::format( - "Cannot start a sync session for user '%1' because this user has been removed.", identity())); + throw app::AppError( + ErrorCodes::ClientUserNotFound, + util::format("Cannot start a sync session for user '%1' because this user has been removed.", + m_identity)); } REALM_ASSERT(m_sync_manager); return m_sync_manager->shared_from_this(); @@ -171,33 +196,22 @@ std::shared_ptr SyncUser::session_for_on_disk_path(const std::strin return locked; } -void SyncUser::update_state_and_tokens(SyncUser::State state, const std::string& access_token, - const std::string& refresh_token) +void SyncUser::log_in(const std::string& access_token, const std::string& refresh_token) { + REALM_ASSERT(!access_token.empty()); + REALM_ASSERT(!refresh_token.empty()); std::vector> sessions_to_revive; { util::CheckedLockGuard lock1(m_mutex); util::CheckedLockGuard lock2(m_tokens_mutex); - m_state = state; - m_access_token = access_token.empty() ? RealmJWT{} : RealmJWT(access_token); - m_refresh_token = refresh_token.empty() ? RealmJWT{} : RealmJWT(refresh_token); - switch (m_state) { - case State::Removed: - // Call set_state() rather than update_state_and_tokens to remove a user. - REALM_UNREACHABLE(); - case State::LoggedIn: - sessions_to_revive = revive_sessions(); - break; - case State::LoggedOut: { - REALM_ASSERT(m_access_token == RealmJWT{}); - REALM_ASSERT(m_refresh_token == RealmJWT{}); - break; - } - } - - m_sync_manager->perform_metadata_update([&, state = m_state.load()](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); - metadata->set_state_and_tokens(state, access_token, refresh_token); + m_state = State::LoggedIn; + m_access_token = RealmJWT(access_token); + m_refresh_token = RealmJWT(refresh_token); + sessions_to_revive = revive_sessions(); + + m_sync_manager->perform_metadata_update([&](const auto& manager) { + auto metadata = manager.get_or_make_user_metadata(m_identity); + metadata->set_state_and_tokens(State::LoggedIn, access_token, refresh_token); }); } // (Re)activate all pending sessions. @@ -210,6 +224,23 @@ void SyncUser::update_state_and_tokens(SyncUser::State state, const std::string& emit_change_to_subscribers(*this); } +void SyncUser::invalidate() +{ + { + util::CheckedLockGuard lock1(m_mutex); + util::CheckedLockGuard lock2(m_tokens_mutex); + m_state = State::Removed; + m_access_token = {}; + m_refresh_token = {}; + + m_sync_manager->perform_metadata_update([&](const auto& manager) { + auto metadata = manager.get_or_make_user_metadata(m_identity); + metadata->set_state_and_tokens(State::Removed, "", ""); + }); + } + emit_change_to_subscribers(*this); +} + std::vector> SyncUser::revive_sessions() { std::vector> sessions_to_revive; @@ -224,74 +255,21 @@ std::vector> SyncUser::revive_sessions() return sessions_to_revive; } -void SyncUser::update_refresh_token(std::string&& token) -{ - std::vector> sessions_to_revive; - { - util::CheckedLockGuard lock(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - switch (m_state) { - case State::Removed: - return; - case State::LoggedIn: - m_refresh_token = RealmJWT(std::move(token)); - break; - case State::LoggedOut: { - m_refresh_token = RealmJWT(std::move(token)); - m_state = State::LoggedIn; - sessions_to_revive = revive_sessions(); - break; - } - } - - m_sync_manager->perform_metadata_update([&, raw_refresh_token = m_refresh_token.token](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); - metadata->set_refresh_token(raw_refresh_token); - }); - } - // (Re)activate all pending sessions. - // Note that we do this after releasing the lock, since the session may - // need to access protected User state in the process of binding itself. - for (auto& session : sessions_to_revive) { - session->revive_if_needed(); - } - - emit_change_to_subscribers(*this); -} - void SyncUser::update_access_token(std::string&& token) { - std::vector> sessions_to_revive; { util::CheckedLockGuard lock(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - switch (m_state) { - case State::Removed: - return; - case State::LoggedIn: - m_access_token = RealmJWT(std::move(token)); - break; - case State::LoggedOut: { - m_access_token = RealmJWT(std::move(token)); - m_state = State::LoggedIn; - sessions_to_revive = revive_sessions(); - break; - } - } + if (m_state != State::LoggedIn) + return; + util::CheckedLockGuard lock2(m_tokens_mutex); + m_access_token = RealmJWT(std::move(token)); m_sync_manager->perform_metadata_update([&, raw_access_token = m_access_token.token](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); + auto metadata = manager.get_or_make_user_metadata(m_identity); metadata->set_access_token(raw_access_token); }); } - // (Re)activate all pending sessions. - // Note that we do this after releasing the lock, since the session may - // need to access protected User state in the process of binding itself. - for (auto& session : sessions_to_revive) { - session->revive_if_needed(); - } - emit_change_to_subscribers(*this); } @@ -301,22 +279,6 @@ std::vector SyncUser::identities() const return m_user_identities; } - -void SyncUser::update_identities(std::vector identities) -{ - util::CheckedLockGuard lock(m_mutex); - if (m_state != SyncUser::State::LoggedIn) { - return; - } - - m_user_identities = identities; - - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); - metadata->set_identities(identities); - }); -} - void SyncUser::log_out() { // We'll extend the lifetime of SyncManager while holding m_mutex so that we know it's safe to call methods on it @@ -324,29 +286,31 @@ void SyncUser::log_out() std::shared_ptr sync_manager_shared; { util::CheckedLockGuard lock(m_mutex); + bool is_anonymous = false; { util::CheckedLockGuard lock2(m_tokens_mutex); if (m_state != State::LoggedIn) { return; } + is_anonymous = do_is_anonymous(); m_state = State::LoggedOut; m_access_token = RealmJWT{}; m_refresh_token = RealmJWT{}; } - if (this->m_provider_type == app::IdentityProviderAnonymous) { + if (is_anonymous) { // An Anonymous user can not log back in. // Mark the user as 'dead' in the persisted metadata Realm. m_state = State::Removed; m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type, false); + auto metadata = manager.get_or_make_user_metadata(m_identity, false); if (metadata) metadata->remove(); }); } else { m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); + auto metadata = manager.get_or_make_user_metadata(m_identity); metadata->set_state_and_tokens(State::LoggedOut, "", ""); }); } @@ -362,25 +326,27 @@ void SyncUser::log_out() m_sessions.clear(); } - sync_manager_shared->log_out_user(m_identity); + sync_manager_shared->log_out_user(*this); emit_change_to_subscribers(*this); } bool SyncUser::is_logged_in() const { util::CheckedLockGuard lock(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - return do_is_logged_in(); + return m_state == State::LoggedIn; } -bool SyncUser::do_is_logged_in() const +bool SyncUser::is_anonymous() const { - return !m_access_token.token.empty() && !m_refresh_token.token.empty() && state() == State::LoggedIn; + util::CheckedLockGuard lock(m_mutex); + util::CheckedLockGuard lock2(m_tokens_mutex); + return do_is_anonymous(); } -void SyncUser::invalidate() +bool SyncUser::do_is_anonymous() const { - set_state(SyncUser::State::Removed); + return m_state == State::LoggedIn && m_user_identities.size() == 1 && + m_user_identities[0].provider_type == app::IdentityProviderAnonymous; } std::string SyncUser::refresh_token() const @@ -407,21 +373,10 @@ bool SyncUser::has_device_id() const return !m_device_id.empty() && m_device_id != "000000000000000000000000"; } -SyncUser::State SyncUser::state() const NO_THREAD_SAFETY_ANALYSIS -{ - return m_state; -} - -void SyncUser::set_state(SyncUser::State state) +SyncUser::State SyncUser::state() const { util::CheckedLockGuard lock(m_mutex); - m_state = state; - - REALM_ASSERT(m_sync_manager); - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); - metadata->set_state(state); - }); + return m_state; } SyncUserProfile SyncUser::user_profile() const @@ -436,18 +391,20 @@ util::Optional SyncUser::custom_data() const return m_access_token.user_data; } -void SyncUser::update_user_profile(const SyncUserProfile& profile) +void SyncUser::update_user_profile(std::vector identities, SyncUserProfile profile) { util::CheckedLockGuard lock(m_mutex); - if (m_state != SyncUser::State::LoggedIn) { + if (m_state == SyncUser::State::Removed) { return; } - m_user_profile = profile; + m_user_identities = std::move(identities); + m_user_profile = std::move(profile); - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, m_provider_type); - metadata->set_user_profile(profile); + m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { + auto metadata = manager.get_or_make_user_metadata(m_identity); + metadata->set_identities(m_user_identities); + metadata->set_user_profile(m_user_profile); }); } @@ -530,7 +487,7 @@ bool SyncUser::access_token_refresh_required() const const auto now = duration_cast(system_clock::now().time_since_epoch()).count() + m_seconds_to_adjust_time_for_testing.load(std::memory_order_relaxed); const auto threshold = now - buffer_seconds; - return do_is_logged_in() && m_access_token.expires_at < static_cast(threshold); + return !m_access_token.token.empty() && m_access_token.expires_at < static_cast(threshold); } } // namespace realm diff --git a/src/realm/object-store/sync/sync_user.hpp b/src/realm/object-store/sync/sync_user.hpp index 1bd5f9e8fca..3f41cc77d99 100644 --- a/src/realm/object-store/sync/sync_user.hpp +++ b/src/realm/object-store/sync/sync_user.hpp @@ -38,8 +38,9 @@ namespace app { struct AppError; class MongoClient; } // namespace app -class SyncSession; class SyncManager; +class SyncSession; +class SyncUserMetadata; // A superclass that bindings can inherit from in order to store information // upon a `SyncUser` object. @@ -75,74 +76,47 @@ struct SyncUserProfile { // The full name of the user. util::Optional name() const { - if (m_data.find("name") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("name")); + return get_field("name"); } // The email address of the user. util::Optional email() const { - if (m_data.find("email") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("email")); + return get_field("email"); } // A URL to the user's profile picture. util::Optional picture_url() const { - if (m_data.find("picture_url") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("picture_url")); + return get_field("picture_url"); } // The first name of the user. util::Optional first_name() const { - if (m_data.find("first_name") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("first_name")); + return get_field("first_name"); } // The last name of the user. util::Optional last_name() const { - if (m_data.find("last_name") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("last_name")); + return get_field("last_name"); } // The gender of the user. util::Optional gender() const { - if (m_data.find("gender") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("gender")); + return get_field("gender"); } // The birthdate of the user. util::Optional birthday() const { - if (m_data.find("birthday") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("birthday")); + return get_field("birthday"); } // The minimum age of the user. util::Optional min_age() const { - if (m_data.find("min_age") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("min_age")); + return get_field("min_age"); } // The maximum age of the user. util::Optional max_age() const { - if (m_data.find("max_age") == m_data.end()) { - return util::none; - } - return static_cast(m_data.at("max_age")); + return get_field("max_age"); } bson::Bson operator[](const std::string& key) const @@ -150,7 +124,7 @@ struct SyncUserProfile { return m_data.at(key); } - bson::BsonDocument data() const + const bson::BsonDocument& data() const { return m_data; } @@ -163,6 +137,15 @@ struct SyncUserProfile { private: bson::BsonDocument m_data; + + util::Optional get_field(const char* name) const + { + auto it = m_data.find(name); + if (it == m_data.end()) { + return util::none; + } + return static_cast((*it).second); + } }; // A struct that represents an identity that a `User` is linked to @@ -197,12 +180,6 @@ class SyncUser : public std::enable_shared_from_this, public Subscriba Removed, }; - // Don't use this directly; use the `SyncManager` APIs. Public for use with `make_shared`. - SyncUser(std::string refresh_token, const std::string id, const std::string provider_type, - std::string access_token, SyncUser::State state, const std::string device_id, SyncManager* sync_manager); - - ~SyncUser(); - // Return a list of all sessions belonging to this user. std::vector> all_sessions() REQUIRES(!m_mutex); @@ -212,72 +189,74 @@ class SyncUser : public std::enable_shared_from_this, public Subscriba // for testing purposes, and for bindings for consumers that are servers or tools. std::shared_ptr session_for_on_disk_path(const std::string& path) REQUIRES(!m_mutex); - // Update the user's state and refresh/access tokens atomically in a Realm transaction. - // If the user is transitioning between LoggedIn and LoggedOut, then the access_token and - // refresh token must be empty, and likewise must not be empty if transitioning between - // logged out and logged in. - // Note that this is called by the SyncManager, and should not be directly called. - void update_state_and_tokens(SyncUser::State state, const std::string& access_token, - const std::string& refresh_token) REQUIRES(!m_mutex, !m_tokens_mutex); - - // Update the user's refresh token. If the user is logged out, it will log itself back in. - // Note that this is called by the SyncManager, and should not be directly called. - void update_refresh_token(std::string&& token) REQUIRES(!m_mutex, !m_tokens_mutex); - - // Update the user's access token. If the user is logged out, it will log itself back in. - // Note that this is called by the SyncManager, and should not be directly called. - void update_access_token(std::string&& token) REQUIRES(!m_mutex, !m_tokens_mutex); - - // Update the user's profile. - void update_user_profile(const SyncUserProfile& profile) REQUIRES(!m_mutex); - - // Update the user's identities. - void update_identities(std::vector identities) REQUIRES(!m_mutex); - // Log the user out and mark it as such. This will also close its associated Sessions. void log_out() REQUIRES(!m_mutex, !m_tokens_mutex); - /// Returns true id the users access_token and refresh_token are set. + /// Returns true if the users access_token and refresh_token are set. bool is_logged_in() const REQUIRES(!m_mutex, !m_tokens_mutex); + /// Returns true if the user's only identity is anonymous. + bool is_anonymous() const REQUIRES(!m_mutex, !m_tokens_mutex); + const std::string& identity() const noexcept { return m_identity; } - const std::string& provider_type() const noexcept - { - return m_provider_type; - } - - const std::string& local_identity() const noexcept + const std::vector& legacy_identities() const noexcept { - return m_local_identity; + return m_legacy_identities; } std::string access_token() const REQUIRES(!m_tokens_mutex); - std::string refresh_token() const REQUIRES(!m_tokens_mutex); - std::string device_id() const REQUIRES(!m_mutex); - bool has_device_id() const REQUIRES(!m_mutex); - SyncUserProfile user_profile() const REQUIRES(!m_mutex); - std::vector identities() const REQUIRES(!m_mutex); + State state() const REQUIRES(!m_mutex); // Custom user data embedded in the access token. util::Optional custom_data() const REQUIRES(!m_tokens_mutex); - State state() const; - void set_state(SyncUser::State state) REQUIRES(!m_mutex); - std::shared_ptr binding_context() const { return m_binding_context.load(); } + // Optionally set a context factory. If so, must be set before any sessions are created. + static void set_binding_context_factory(SyncUserContextFactory factory); + + std::shared_ptr sync_manager() const REQUIRES(!m_mutex); + + /// Retrieves a general-purpose service client for the Realm Cloud service + /// @param service_name The name of the cluster + app::MongoClient mongo_client(const std::string& service_name) REQUIRES(!m_mutex); + + // ------------------------------------------------------------------------ + // All of the following are called by `SyncManager` and are public only for + // testing purposes. SDKs should not call these directly in non-test code + // or expose them in the public API. + + // Don't use this directly; use the `SyncManager` APIs. Public for use with `make_shared`. + SyncUser(const std::string& refresh_token, const std::string& id, const std::string& access_token, + const std::string& device_id, SyncManager* sync_manager); + SyncUser(const SyncUserMetadata& data, SyncManager* sync_manager); + + // Atomically set the user to be logged in and update both tokens. + void log_in(const std::string& access_token, const std::string& refresh_token) + REQUIRES(!m_mutex, !m_tokens_mutex); + + // Atomically set the user to be removed and remove tokens. + void invalidate() REQUIRES(!m_mutex, !m_tokens_mutex); + + // Update the user's access token. If the user is logged out, it will log itself back in. + // Note that this is called by the SyncManager, and should not be directly called. + void update_access_token(std::string&& token) REQUIRES(!m_mutex, !m_tokens_mutex); + + // Update the user's profile and identities. + void update_user_profile(std::vector identities, SyncUserProfile profile) REQUIRES(!m_mutex); + // Register a session to this user. // A registered session will be bound at the earliest opportunity: either // immediately, or upon the user becoming Active. @@ -285,7 +264,7 @@ class SyncUser : public std::enable_shared_from_this, public Subscriba void register_session(std::shared_ptr) REQUIRES(!m_mutex); /// Refreshes the custom data for this user - /// If update_location is true, the location metadata will be queried before the request + /// If `update_location` is true, the location metadata will be queried before the request void refresh_custom_data(bool update_location, util::UniqueFunction)> completion_block) REQUIRES(!m_mutex); @@ -296,31 +275,12 @@ class SyncUser : public std::enable_shared_from_this, public Subscriba /// true. bool access_token_refresh_required() const REQUIRES(!m_tokens_mutex); - // Optionally set a context factory. If so, must be set before any sessions are created. - static void set_binding_context_factory(SyncUserContextFactory factory); - - std::shared_ptr sync_manager() const REQUIRES(!m_mutex); - - /// Retrieves a general-purpose service client for the Realm Cloud service - /// @param service_name The name of the cluster - app::MongoClient mongo_client(const std::string& service_name) REQUIRES(!m_mutex); - + // Hook for testing access token timeouts void set_seconds_to_adjust_time_for_testing(int seconds) { m_seconds_to_adjust_time_for_testing.store(seconds); } - /// Check the SyncUsers passed as argument have the same remote identity id. - friend bool operator==(const SyncUser& lhs, const SyncUser& rhs) - { - return lhs.identity() == rhs.identity(); - } - - friend bool operator!=(const SyncUser& lhs, const SyncUser& rhs) - { - return !(lhs == rhs); - } - protected: friend class SyncManager; void detach_from_sync_manager() REQUIRES(!m_mutex); @@ -329,22 +289,17 @@ class SyncUser : public std::enable_shared_from_this, public Subscriba static SyncUserContextFactory s_binding_context_factory; static std::mutex s_binding_context_factory_mutex; - bool do_is_logged_in() const REQUIRES(m_tokens_mutex); + bool do_is_anonymous() const REQUIRES(m_mutex); std::vector> revive_sessions() REQUIRES(m_mutex); - std::atomic m_state GUARDED_BY(m_mutex); + State m_state GUARDED_BY(m_mutex); util::AtomicSharedPtr m_binding_context; - // A locally assigned UUID intended to provide a level of indirection for various features. - std::string m_local_identity; - - // The auth provider used to login this user. - const std::string m_provider_type; - - // Mark the user as invalid, since a fatal user-related error was encountered. - void invalidate() REQUIRES(!m_mutex); + // UUIDs which used to be used to generate local Realm file paths. Now only + // used to locate existing files. + std::vector m_legacy_identities; mutable util::CheckedMutex m_mutex; diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index f249c5f011f..e07c4268caa 100644 --- a/test/object-store/CMakeLists.txt +++ b/test/object-store/CMakeLists.txt @@ -54,13 +54,15 @@ endif() if(REALM_ENABLE_SYNC) list(APPEND HEADERS + util/sync/baas_admin_api.hpp util/sync/flx_sync_harness.hpp util/sync/session_util.hpp util/sync/sync_test_utils.hpp - util/sync/baas_admin_api.hpp + util/unit_test_transport.hpp ) list(APPEND SOURCES bson.cpp + sync/app.cpp sync/client_reset.cpp sync/file.cpp sync/flx_migration.cpp @@ -74,8 +76,8 @@ if(REALM_ENABLE_SYNC) sync/session/wait_for_completion.cpp sync/sync_manager.cpp sync/user.cpp - sync/app.cpp util/sync/sync_test_utils.cpp + util/unit_test_transport.cpp ) if(APPLE) list(APPEND SOURCES audit.cpp) diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 7ad3e1be116..481dec645e6 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -16,21 +16,18 @@ // //////////////////////////////////////////////////////////////////////////// -#include -#include -#include +#include "util/test_file.hpp" +#include "util/event_loop.hpp" #include -#include #include #include #include #include +#include #include - #include - #include #include @@ -42,21 +39,22 @@ #include #if REALM_ENABLE_SYNC +#include "util/sync/flx_sync_harness.hpp" +#include "util/sync/sync_test_utils.hpp" +#include "util/unit_test_transport.hpp" + +#include #include +#include +#include +#include +#include #include #endif #if REALM_ENABLE_AUTH_TESTS -#include -#include - -#include - -#include -#include - -#include +#include "util/sync/baas_admin_api.hpp" #endif using namespace realm; @@ -308,126 +306,6 @@ CPtr clone_cptr(const T* ptr) } \ } while (false); -#if REALM_ENABLE_AUTH_TESTS -class CApiUnitTestTransport : public app::GenericNetworkTransport { - std::string m_provider_type; - -public: - CApiUnitTestTransport(const std::string& provider_type = {}, uint64_t request_timeout = 60000) - : m_provider_type(provider_type.empty() ? "anon-user" : provider_type) - , request_timeout(request_timeout) - { - profile_0 = nlohmann::json({{"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"}}); - } - - explicit CApiUnitTestTransport(const uint64_t request_timeout) - : CApiUnitTestTransport({}, request_timeout) - { - } - - void set_provider_type(const std::string& provider_type) - { - m_provider_type = provider_type; - } - - const std::string access_token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2Rldklk" - "IjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoi" - "NWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s"; - const std::string user_id = "awelfkewjfewkefkeafj"; - const std::string identity_0_id = "eflkjf393flkj33fjf3"; - const std::string identity_1_id = "aewfjklewfwoifejjef"; - nlohmann::json profile_0; - uint64_t request_timeout; - - -private: - void handle_profile(const app::Request&, util::UniqueFunction&& completion) - { - std::string response = - nlohmann::json({{"user_id", user_id}, - {"identities", - {{{"id", identity_0_id}, {"provider_type", m_provider_type}, {"provider_id", "lol"}}, - {{"id", identity_1_id}, {"provider_type", "lol_wut"}, {"provider_id", "nah_dawg"}}}}, - {"data", profile_0}}) - .dump(); - - completion(app::Response{200, 0, {}, response}); - } - - void handle_login(const app::Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == app::HttpMethod::post); - auto item = app::AppUtils::find_header("Content-Type", request.headers); - CHECK(item); - CHECK(item->second == "application/json;charset=utf-8"); - // Verify against - CHECK(nlohmann::json::parse(request.body)["options"] == - nlohmann::json({{"device", - {{"appId", "app_id_123"}, - {"platform", util::get_library_platform()}, - {"platformVersion", "some_platform_version"}, - {"sdk", "some_sdk_name"}, - {"sdkVersion", "some_sdk_version"}, - {"cpuArch", util::get_library_cpu_arch()}, - {"deviceName", "some_device_name"}, - {"deviceVersion", "some_device_version"}, - {"frameworkName", "some_framework_name"}, - {"frameworkVersion", "some_framework_version"}, - {"coreVersion", REALM_VERSION_STRING}, - {"bundleId", "some_bundle_id"}}}})); - - CHECK(request.timeout_ms == request_timeout); - - std::string response = nlohmann::json({{"access_token", access_token}, - {"refresh_token", access_token}, - {"user_id", user_id}, - {"device_id", "Panda Bear"}}) - .dump(); - - completion(app::Response{200, 0, {}, response}); - } - - void handle_location(const app::Request&, util::UniqueFunction&& completion) - { - std::string response = nlohmann::json({{"deployment_model", "this"}, - {"hostname", "field"}, - {"ws_hostname", "shouldn't"}, - {"location", "matter"}}) - .dump(); - - completion(app::Response{200, 0, {}, response}); - } - -public: - void send_request_to_server(const app::Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - handle_login(request, std::move(completion)); - } - else if (request.url.find("/profile") != std::string::npos) { - handle_profile(request, std::move(completion)); - } - else if (request.url.find("/location") != std::string::npos && request.method == app::HttpMethod::get) { - handle_location(request, std::move(completion)); - } - else { - completion(app::Response{200, 0, {}, "something arbitrary"}); - } - } -}; -#endif // REALM_ENABLE_AUTH_TESTS - TEST_CASE("C API (C)", "[c_api]") { TestFile file; CHECK(realm_c_api_tests(file.path.c_str()) == 0); @@ -644,11 +522,24 @@ TEST_CASE("C API (non-database)", "[c_api]") { } } -#if REALM_ENABLE_AUTH_TESTS +#if REALM_ENABLE_SYNC SECTION("realm_app_config_t") { const uint64_t request_timeout = 2500; - std::shared_ptr transport = - std::make_shared(request_timeout); + auto transport = std::make_shared(request_timeout); + transport->set_expected_options({{"device", + {{"appId", "app_id_123"}, + {"platform", util::get_library_platform()}, + {"platformVersion", "some_platform_version"}, + {"sdk", "some_sdk_name"}, + {"sdkVersion", "some_sdk_version"}, + {"cpuArch", util::get_library_cpu_arch()}, + {"deviceName", "some_device_name"}, + {"deviceVersion", "some_device_version"}, + {"frameworkName", "some_framework_name"}, + {"frameworkVersion", "some_framework_version"}, + {"coreVersion", REALM_VERSION_STRING}, + {"bundleId", "some_bundle_id"}}}}); + auto http_transport = realm_http_transport(transport); auto app_config = cptr(realm_app_config_new("app_id_123", &http_transport)); CHECK(app_config.get() != nullptr); @@ -693,8 +584,7 @@ TEST_CASE("C API (non-database)", "[c_api]") { CHECK(!error); }); } - -#endif // REALM_ENABLE_AUTH_TESTS +#endif // REALM_ENABLE_SYNC } namespace { @@ -5116,8 +5006,7 @@ TEST_CASE("C API - async_open", "[sync][pbs][c_api]") { realm_sync_config_t* sync_config = realm_sync_config_new(&user, "realm"); realm_sync_config_set_initial_subscription_handler(sync_config, task_init_subscription, false, nullptr, nullptr); - sync_config->user->update_refresh_token(std::string(invalid_token)); - sync_config->user->update_access_token(std::move(invalid_token)); + sync_config->user->log_in(invalid_token, invalid_token); realm_config_set_path(config, test_config.path.c_str()); realm_config_set_schema_version(config, 1); @@ -5245,7 +5134,7 @@ TEST_CASE("C API - binding callback thread observer", "[sync][c_api]") { } #endif -#ifdef REALM_ENABLE_AUTH_TESTS +#if REALM_ENABLE_AUTH_TESTS std::atomic_bool baas_client_stop{false}; std::atomic error_handler_counter{0}; @@ -5517,7 +5406,6 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a realm_app_credentials anonymous(app::AppCredentials::anonymous()); realm_app_log_in_with_credentials(&app, &anonymous, realm_app_user1, &sync_user_1, nullptr); - CHECK(realm_user_get_auth_provider(sync_user_1) == RLM_AUTH_PROVIDER_ANONYMOUS); realm_app_remove_user(&app, sync_user_1, realm_app_void_completion, nullptr, nullptr); auto state = realm_user_get_state(sync_user_1); CHECK(state == RLM_USER_STATE_REMOVED); @@ -5532,7 +5420,6 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a realm_app_credentials anonymous(app::AppCredentials::anonymous()); realm_app_log_in_with_credentials(&app, &anonymous, realm_app_user1, &sync_user, nullptr); - CHECK(realm_user_get_auth_provider(sync_user) == RLM_AUTH_PROVIDER_ANONYMOUS); realm_app_delete_user(&app, sync_user, realm_app_void_completion, nullptr, nullptr); auto state = realm_user_get_state(sync_user); CHECK(state == RLM_USER_STATE_REMOVED); @@ -5550,8 +5437,6 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a realm_app_credentials anonymous(app::AppCredentials::anonymous()); realm_app_log_in_with_credentials(&app, &anonymous, realm_app_user1, &sync_user, nullptr); - CHECK(realm_user_get_auth_provider(sync_user) == RLM_AUTH_PROVIDER_ANONYMOUS); - realm_app_credentials email_creds(creds); realm_app_link_user(&app, sync_user, &email_creds, realm_app_user2, &processed, nullptr); @@ -5567,7 +5452,6 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a realm_app_void_completion, nullptr, nullptr); realm_app_credentials anonymous(app::AppCredentials::anonymous()); realm_app_log_in_with_credentials(&app, &anonymous, realm_app_user1, &sync_user_1, nullptr); - CHECK(realm_user_get_auth_provider(sync_user_1) == RLM_AUTH_PROVIDER_ANONYMOUS); auto current_user = realm_app_get_current_user(&app); CHECK(realm_equals(sync_user_1, current_user)); realm_release(current_user); @@ -5599,7 +5483,6 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a realm_app_void_completion, nullptr, nullptr); realm_app_credentials anonymous(app::AppCredentials::anonymous()); realm_app_log_in_with_credentials(&app, &anonymous, realm_app_user1, &sync_user_1, nullptr); - CHECK(realm_user_get_auth_provider(sync_user_1) == RLM_AUTH_PROVIDER_ANONYMOUS); auto callback = [](realm_userdata_t, realm_app_user_apikey_t[], size_t count, realm_app_error_t* error) { CHECK(error); CHECK(count == 0); diff --git a/test/object-store/realm.cpp b/test/object-store/realm.cpp index 1001eb0c7e5..4cd47753832 100644 --- a/test/object-store/realm.cpp +++ b/test/object-store/realm.cpp @@ -1184,8 +1184,7 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { TestSyncManager tsm(tsm_config); SyncTestFile config(tsm.app(), "realm"); - config.sync_config->user->update_refresh_token(std::string(invalid_token)); - config.sync_config->user->update_access_token(std::move(invalid_token)); + config.sync_config->user->log_in(invalid_token, invalid_token); bool got_error = false; config.sync_config->error_handler = [&](std::shared_ptr, SyncError) { diff --git a/test/object-store/sync-metadata-v6.realm b/test/object-store/sync-metadata-v6.realm new file mode 100644 index 00000000000..8f3fde476bc Binary files /dev/null and b/test/object-store/sync-metadata-v6.realm differ diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp index a97761be4c2..5251e180bda 100644 --- a/test/object-store/sync/app.cpp +++ b/test/object-store/sync/app.cpp @@ -16,50 +16,40 @@ // //////////////////////////////////////////////////////////////////////////// -#include -#include -#include -#include -#include -#include +#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 #include #include -#include #include #include #include -#include using namespace realm; using namespace realm::app; @@ -72,6 +62,9 @@ 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); @@ -351,13 +344,12 @@ TEST_CASE("app: login_with_credentials integration", "[sync][app][user][baas]") int subscribe_processed = 0; auto token = app->subscribe([&subscribe_processed](auto& app) { if (!subscribe_processed) { - subscribe_processed++; - REQUIRE(static_cast(app.current_user())); + REQUIRE(app.current_user()); } else { - subscribe_processed++; - REQUIRE(!static_cast(app.current_user())); + REQUIRE_FALSE(app.current_user()); } + subscribe_processed++; }); auto user = log_in(app); @@ -827,40 +819,60 @@ TEST_CASE("app: auth providers function integration", "[sync][app][user][baas]") bson::BsonDocument function_params{{"realmCustomAuthFuncUserId", "123456"}}; auto credentials = AppCredentials::function(function_params); auto user = log_in(app, credentials); - REQUIRE(user->provider_type() == IdentityProviderFunction); + REQUIRE(user->identities()[0].provider_type == IdentityProviderFunction); } } // MARK: - Link User Tests -TEST_CASE("app: link_user integration", "[sync][app][user][baas]") { +TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { TestAppSession session; auto app = session.app(); + auto user = log_in(app); - SECTION("link_user integration") { - AutoVerifiedEmailCredentials creds; - bool processed = false; - std::shared_ptr sync_user; + AutoVerifiedEmailCredentials creds; + app->provider_client().register_email(creds.email, creds.password, + [&](Optional error) { + REQUIRE_FALSE(error); + }); - app->provider_client().register_email( - creds.email, creds.password, [&](Optional error) { - CAPTURE(creds.email); - CAPTURE(creds.password); - REQUIRE_FALSE(error); // first registration success - }); + SECTION("anonymous users are reused before they are linked to an identity") { + REQUIRE(user == log_in(app)); + } - sync_user = log_in(app); - CHECK(sync_user->provider_type() == IdentityProviderAnonymous); + 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(sync_user, creds, [&](std::shared_ptr user, Optional error) { + app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { REQUIRE_FALSE(error); - REQUIRE(user); - CHECK(user->identity() == sync_user->identity()); - CHECK(user->identities().size() == 2); - processed = true; + REQUIRE(user == user2); + REQUIRE(user->identities().size() == 2); + CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); + CHECK(user->identities()[1].provider_type == IdentityProviderUsernamePassword); }); + } - CHECK(processed); + 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); } } @@ -2856,8 +2868,8 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { std::shared_ptr user = app->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 locallaly - // (access tokens have an lifetime of 30 mintutes today). + // 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()); @@ -2970,6 +2982,17 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { 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; @@ -3181,11 +3204,10 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { anon_user->identity())); }); - REQUIRE_THROWS_MATCHES( - Realm::get_shared_realm(config), std::logic_error, - Catch::Matchers::Message( - util::format("Cannot start a sync session for user '%1' because this 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") { @@ -3204,11 +3226,11 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { REQUIRE_FALSE(email_user->is_logged_in()); REQUIRE(email_user->state() == SyncUser::State::Removed); - // should not be able to open a sync'd Realm with an invalid user - REQUIRE_THROWS_MATCHES( - Realm::get_shared_realm(config), std::logic_error, - Catch::Matchers::Message(util::format( - "Cannot start a sync session for user '%1' because this user has been removed.", user_ident))); + // 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 @@ -3801,7 +3823,6 @@ TEST_CASE("app: custom error handling", "[sync][app][custom errors]") { } } - 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"; @@ -3832,208 +3853,13 @@ static nlohmann::json user_profile_json(std::string user_id = random_string(15), { return {{"user_id", user_id}, {"identities", - {{{"id", identity_0_id}, {"provider_type", provider_type}, {"provider_id", "lol"}}, - {{"id", identity_1_id}, {"provider_type", "lol_wut"}, {"provider_id", "nah_dawg"}}}}, + {{{"id", identity_0_id}, {"provider_type", provider_type}}, + {{"id", identity_1_id}, {"provider_type", "lol_wut"}}}}, {"data", profile_0}}; } // MARK: - Unit Tests -class UnitTestTransport : public GenericNetworkTransport { - std::string m_provider_type; - -public: - UnitTestTransport(const std::string& provider_type = "anon-user") - : m_provider_type(provider_type) - { - } - - static std::string access_token; - - static const std::string api_key; - static const std::string api_key_id; - static const std::string api_key_name; - static const std::string auth_route; - static const std::string user_id; - static const std::string identity_0_id; - static const std::string identity_1_id; - - void set_provider_type(const std::string& provider_type) - { - m_provider_type = provider_type; - } - -private: - void handle_profile(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::get); - auto content_type = AppUtils::find_header("Content-Type", request.headers); - CHECK(content_type); - CHECK(content_type->second == "application/json;charset=utf-8"); - auto authorization = AppUtils::find_header("Authorization", request.headers); - CHECK(authorization); - CHECK(authorization->second == "Bearer " + access_token); - CHECK(request.body.empty()); - CHECK(request.timeout_ms == 60000); - - std::string response = - nlohmann::json({{"user_id", user_id}, - {"identities", - {{{"id", identity_0_id}, {"provider_type", m_provider_type}, {"provider_id", "lol"}}, - {{"id", identity_1_id}, {"provider_type", "lol_wut"}, {"provider_id", "nah_dawg"}}}}, - {"data", profile_0}}) - .dump(); - - completion(Response{200, 0, {}, response}); - } - - void handle_login(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::post); - auto item = AppUtils::find_header("Content-Type", request.headers); - CHECK(item); - CHECK(item->second == "application/json;charset=utf-8"); - CHECK(nlohmann::json::parse(request.body)["options"] == - nlohmann::json({{"device", - {{"appId", "app_id"}, - {"platform", util::get_library_platform()}, - {"platformVersion", "Object Store Test Platform Version"}, - {"sdk", "SDK Name"}, - {"sdkVersion", "SDK Version"}, - {"cpuArch", util::get_library_cpu_arch()}, - {"deviceName", "Device Name"}, - {"deviceVersion", "Device Version"}, - {"frameworkName", "Framework Name"}, - {"frameworkVersion", "Framework Version"}, - {"coreVersion", REALM_VERSION_STRING}, - {"bundleId", "Bundle Id"}}}})); - - CHECK(request.timeout_ms == 60000); - - std::string response = nlohmann::json({{"access_token", access_token}, - {"refresh_token", access_token}, - {"user_id", random_string(15)}, - {"device_id", "Panda Bear"}}) - .dump(); - - completion(Response{200, 0, {}, response}); - } - - void handle_location(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::get); - CHECK(request.timeout_ms == 60000); - - std::string response = nlohmann::json({{"deployment_model", "this"}, - {"hostname", "field"}, - {"ws_hostname", "shouldn't"}, - {"location", "matter"}}) - .dump(); - - completion(Response{200, 0, {}, response}); - } - - void handle_create_api_key(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::post); - auto item = AppUtils::find_header("Content-Type", request.headers); - CHECK(item); - CHECK(item->second == "application/json;charset=utf-8"); - CHECK(nlohmann::json::parse(request.body) == nlohmann::json({{"name", api_key_name}})); - CHECK(request.timeout_ms == 60000); - - std::string response = - nlohmann::json({{"_id", api_key_id}, {"key", api_key}, {"name", api_key_name}, {"disabled", false}}) - .dump(); - - completion(Response{200, 0, {}, response}); - } - - void handle_fetch_api_key(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::get); - auto item = AppUtils::find_header("Content-Type", request.headers); - CHECK(item); - CHECK(item->second == "application/json;charset=utf-8"); - - CHECK(request.body == ""); - CHECK(request.timeout_ms == 60000); - - std::string response = - nlohmann::json({{"_id", api_key_id}, {"name", api_key_name}, {"disabled", false}}).dump(); - - completion(Response{200, 0, {}, response}); - } - - void handle_fetch_api_keys(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::get); - auto item = AppUtils::find_header("Content-Type", request.headers); - CHECK(item); - CHECK(item->second == "application/json;charset=utf-8"); - - CHECK(request.body == ""); - CHECK(request.timeout_ms == 60000); - - auto elements = std::vector(); - for (int i = 0; i < 2; i++) { - elements.push_back({{"_id", api_key_id}, {"name", api_key_name}, {"disabled", false}}); - } - - completion(Response{200, 0, {}, nlohmann::json(elements).dump()}); - } - - void handle_token_refresh(const Request& request, util::UniqueFunction&& completion) - { - CHECK(request.method == HttpMethod::post); - auto item = AppUtils::find_header("Content-Type", request.headers); - CHECK(item); - CHECK(item->second == "application/json;charset=utf-8"); - - CHECK(request.body == ""); - CHECK(request.timeout_ms == 60000); - - auto elements = std::vector(); - nlohmann::json json{{"access_token", access_token}}; - - completion(Response{200, 0, {}, json.dump()}); - } - -public: - void send_request_to_server(const Request& request, - util::UniqueFunction&& completion) override - { - if (request.url.find("/login") != std::string::npos) { - handle_login(request, std::move(completion)); - } - else if (request.url.find("/profile") != std::string::npos) { - handle_profile(request, std::move(completion)); - } - else if (request.url.find("/session") != std::string::npos && request.method != HttpMethod::post) { - completion(Response{200, 0, {}, ""}); - } - else if (request.url.find("/api_keys") != std::string::npos && request.method == HttpMethod::post) { - handle_create_api_key(request, std::move(completion)); - } - else if (request.url.find(util::format("/api_keys/%1", api_key_id)) != std::string::npos && - request.method == HttpMethod::get) { - handle_fetch_api_key(request, std::move(completion)); - } - else if (request.url.find("/api_keys") != std::string::npos && request.method == HttpMethod::get) { - handle_fetch_api_keys(request, std::move(completion)); - } - else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { - handle_token_refresh(request, std::move(completion)); - } - else if (request.url.find("/location") != std::string::npos && request.method == HttpMethod::get) { - handle_location(request, std::move(completion)); - } - else { - completion(Response{200, 0, {}, "something arbitrary"}); - } - } -}; - static TestSyncManager::Config get_config() { return get_config(instance_of); @@ -4051,19 +3877,9 @@ static const std::string good_access_token2 = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU2YmJi" "YzBhNmI3ZGZkM2UyNTA0OGIzIiwidHlwIjoiYWNjZXNzIn0.eSX4QMjIOLbdOYOPzQrD_racwLUk1HGFgxtx2a34k80"; -std::string UnitTestTransport::access_token = good_access_token; - static const std::string bad_access_token = "lolwut"; static const std::string dummy_device_id = "123400000000000000000000"; -const std::string UnitTestTransport::api_key = "lVRPQVYBJSIbGos2ZZn0mGaIq1SIOsGaZ5lrcp8bxlR5jg4OGuGwQq1GkektNQ3i"; -const std::string UnitTestTransport::api_key_id = "5e5e6f0abe4ae2a2c2c2d329"; -const std::string UnitTestTransport::api_key_name = "some_api_key_name"; -const std::string UnitTestTransport::auth_route = "https://mongodb.com/unittests"; -const std::string UnitTestTransport::user_id = "Ailuropoda melanoleuca"; -const std::string UnitTestTransport::identity_0_id = "Ursus arctos isabellinus"; -const std::string UnitTestTransport::identity_1_id = "Ursus arctos horribilis"; - TEST_CASE("subscribable unit tests", "[sync][app]") { struct Foo : public Subscribable { void event() @@ -4135,6 +3951,7 @@ TEST_CASE("subscribable unit tests", "[sync][app]") { 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; @@ -4146,9 +3963,8 @@ TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { auto user = log_in(app); - CHECK(user->identities().size() == 2); + REQUIRE(user->identities().size() == 1); CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); - CHECK(user->identities()[1].id == UnitTestTransport::identity_1_id); SyncUserProfile user_profile = user->user_profile(); CHECK(user_profile.name() == profile_0_name); @@ -4168,9 +3984,8 @@ TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { auto app = tsm.app(); REQUIRE(app->all_users().size() == 1); auto user = app->all_users()[0]; - CHECK(user->identities().size() == 2); + REQUIRE(user->identities().size() == 1); CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); - CHECK(user->identities()[1].id == UnitTestTransport::identity_1_id); SyncUserProfile user_profile = user->user_profile(); CHECK(user_profile.name() == profile_0_name); @@ -4226,8 +4041,8 @@ TEST_CASE("app: UserAPIKeyProviderClient unit_tests", "[sync][app][user][api key auto app = sync_manager.app(); auto client = app->provider_client(); - std::shared_ptr logged_in_user = app->sync_manager()->get_user( - UnitTestTransport::user_id, good_access_token, good_access_token, "anon-user", dummy_device_id); + std::shared_ptr logged_in_user = + app->sync_manager()->get_user("userid", good_access_token, good_access_token, dummy_device_id); bool processed = false; ObjectId obj_id(UnitTestTransport::api_key_id.c_str()); @@ -4310,11 +4125,11 @@ TEST_CASE("app: user_semantics", "[sync][app][user]") { 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->identity() != user2->identity()); + CHECK(user1 != user2); - // shuold reuse existing session + // should reuse existing session const auto user3 = login_user_anonymous(); - CHECK(user3->identity() == user1->identity()); + CHECK(user3 == user1); auto user_events_processed = 0; auto _ = user3->subscribe([&user_events_processed](auto&) { @@ -4638,7 +4453,8 @@ TEST_CASE("app: link_user", "[sync][app][user]") { auto email_pass_credentials = AppCredentials::username_password(email, password); auto sync_user = log_in(app, email_pass_credentials); - CHECK(sync_user->provider_type() == IdentityProviderUsernamePassword); + REQUIRE(sync_user->identities().size() == 2); + CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword); SECTION("successful link") { bool processed = false; @@ -4751,8 +4567,7 @@ TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") { if (app->sync_manager()->get_current_user()) { return; } - app->sync_manager()->get_user("a_user_id", good_access_token, good_access_token, "anon-user", - dummy_device_id); + app->sync_manager()->get_user("a_user_id", good_access_token, good_access_token, dummy_device_id); }; SECTION("refresh custom data happy path") { @@ -5086,7 +4901,7 @@ TEST_CASE("app: app destroyed during token refresh", "[sync][app][user][token]") // 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"}; + "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; @@ -5457,8 +5272,7 @@ TEST_CASE("app: user logs out while profile is fetched", "[sync][app][user]") { TestSyncManager sync_manager(get_config(transporter)); auto app = sync_manager.app(); - logged_in_user = app->sync_manager()->get_user(UnitTestTransport::user_id, good_access_token, good_access_token, - "anon-user", dummy_device_id); + logged_in_user = app->sync_manager()->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>(); diff --git a/test/object-store/sync/file.cpp b/test/object-store/sync/file.cpp index 3401bc6689e..4d9cf0b1fd5 100644 --- a/test/object-store/sync/file.cpp +++ b/test/object-store/sync/file.cpp @@ -135,7 +135,8 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { TestSyncManager tsm; const std::string identity = "abcdefghi"; - const std::string local_identity = "123456789"; + const std::vector legacy_identities = {"legacy1", "legacy2"}; + const auto& local_identity = legacy_identities[0]; const std::string app_id = "test_app_id*$#@!%1"; const std::string partition_str = random_string(10); const std::string partition = bson::Bson(partition_str).to_string(); @@ -147,22 +148,22 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { SECTION("Realm path APIs") { auto relative_path = "s_" + partition_str; - ExpectedRealmPaths expected_paths(manager_base_path.string(), app_id, identity, local_identity, partition); + ExpectedRealmPaths expected_paths(manager_base_path.string(), app_id, identity, legacy_identities, partition); SECTION("getting a Realm path") { - auto actual = manager.realm_file_path(identity, local_identity, relative_path, partition); + auto actual = manager.realm_file_path(identity, legacy_identities, relative_path, partition); REQUIRE(expected_paths.current_preferred_path == actual); } SECTION("deleting a Realm for a valid user") { - manager.realm_file_path(identity, local_identity, relative_path, partition); + manager.realm_file_path(identity, legacy_identities, relative_path, partition); // Create the required files REQUIRE(create_dummy_realm(expected_paths.current_preferred_path)); REQUIRE(File::exists(expected_paths.current_preferred_path)); REQUIRE(File::exists(expected_paths.current_preferred_path + ".lock")); REQUIRE_DIR_EXISTS(expected_paths.current_preferred_path + ".management"); // Delete the Realm - REQUIRE(manager.remove_realm(identity, local_identity, relative_path, partition)); + REQUIRE(manager.remove_realm(identity, legacy_identities, relative_path, partition)); // Ensure the files don't exist anymore REQUIRE(!File::exists(expected_paths.current_preferred_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path + ".lock")); @@ -170,7 +171,7 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { } SECTION("deleting a Realm for an invalid user") { - REQUIRE(!manager.remove_realm("invalid_user", "invalid_ident", relative_path, partition)); + REQUIRE(!manager.remove_realm("invalid_user", legacy_identities, relative_path, partition)); } SECTION("hashed path is used if it already exists") { @@ -181,7 +182,7 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(create_dummy_realm(expected_paths.fallback_hashed_path)); REQUIRE(File::exists(expected_paths.fallback_hashed_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - auto actual = manager.realm_file_path(identity, local_identity, relative_path, partition); + auto actual = manager.realm_file_path(identity, legacy_identities, relative_path, partition); REQUIRE(actual == expected_paths.fallback_hashed_path); REQUIRE(File::exists(expected_paths.fallback_hashed_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); @@ -198,7 +199,7 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(File::exists(expected_paths.legacy_local_id_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - auto actual = manager.realm_file_path(identity, local_identity, relative_path, partition); + auto actual = manager.realm_file_path(identity, legacy_identities, relative_path, partition); REQUIRE(actual == expected_paths.legacy_local_id_path); REQUIRE(File::exists(expected_paths.legacy_local_id_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); @@ -206,6 +207,28 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(!File::exists(expected_paths.legacy_local_id_path)); } + SECTION("multiple legacy local identities are supported") { + // ExpectedRealmPaths uses the first legacy identity, so construct + // a second one with only the second identity + const std::vector legacy_identities_2 = {"legacy2"}; + const auto& local_identity_2 = legacy_identities_2[0]; + ExpectedRealmPaths expected_paths_2(manager_base_path.string(), app_id, identity, legacy_identities_2, + partition); + + util::try_make_dir(manager_path.string()); + util::try_make_dir((manager_path / local_identity_2).string()); + REQUIRE(create_dummy_realm(expected_paths_2.legacy_local_id_path)); + + // Note: intentionally not legacy_identities_2. We're passing both + // in and validating that it'll open the second one. + auto actual = manager.realm_file_path(identity, legacy_identities, relative_path, partition); + REQUIRE(actual == expected_paths_2.legacy_local_id_path); + REQUIRE(File::exists(expected_paths_2.legacy_local_id_path)); + REQUIRE(!File::exists(expected_paths_2.current_preferred_path)); + manager.remove_user_realms(identity, {expected_paths_2.legacy_local_id_path}); + REQUIRE(!File::exists(expected_paths_2.legacy_local_id_path)); + } + SECTION("legacy sync paths are detected and used") { REQUIRE(!File::exists(expected_paths.legacy_sync_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); @@ -215,7 +238,7 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(create_dummy_realm(expected_paths.legacy_sync_path)); REQUIRE(File::exists(expected_paths.legacy_sync_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - auto actual = manager.realm_file_path(identity, local_identity, relative_path, partition); + auto actual = manager.realm_file_path(identity, legacy_identities, relative_path, partition); REQUIRE(actual == expected_paths.legacy_sync_path); REQUIRE(File::exists(expected_paths.legacy_sync_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); @@ -226,7 +249,7 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { SECTION("paths have a fallback hashed location if the preferred path is too long") { const std::string long_path_name = std::string(500, 'a'); REQUIRE(long_path_name.length() > 255); // linux name length limit - auto actual = manager.realm_file_path(identity, local_identity, long_path_name, partition); + auto actual = manager.realm_file_path(identity, legacy_identities, long_path_name, partition); REQUIRE(actual.length() < 500); REQUIRE(create_dummy_realm(actual)); REQUIRE(File::exists(actual)); diff --git a/test/object-store/sync/flx_sync.cpp b/test/object-store/sync/flx_sync.cpp index f90abbea240..1ca2bc301f6 100644 --- a/test/object-store/sync/flx_sync.cpp +++ b/test/object-store/sync/flx_sync.cpp @@ -612,7 +612,8 @@ TEST_CASE("flx: client reset", "[sync][flx][client reset][baas]") { util::try_remove_dir_recursive(fresh_path); auto config_copy = config_local; - config_local.sync_config->error_handler = nullptr; + config_copy.sync_config = std::make_shared(*config_copy.sync_config); + config_copy.sync_config->error_handler = nullptr; auto&& [reset_future, reset_handler] = make_client_reset_handler(); config_copy.sync_config->notify_after_client_reset = reset_handler; @@ -3735,7 +3736,7 @@ TEST_CASE("flx: bootstrap changesets are applied continuously", "[sync][flx][boo CHECK(user_commit_version == bootstrap_version + 1); } -TEST_CASE("flx: open realm + register subscription callack while bootstrapping", +TEST_CASE("flx: open realm + register subscription callback while bootstrapping", "[sync][flx][bootstrap][async open][baas]") { FLXSyncTestHarness harness("flx_bootstrap_batching"); auto foo_obj_id = ObjectId::gen(); diff --git a/test/object-store/sync/metadata.cpp b/test/object-store/sync/metadata.cpp index e0eda4d8ce4..919bfdc3111 100644 --- a/test/object-store/sync/metadata.cpp +++ b/test/object-store/sync/metadata.cpp @@ -47,64 +47,56 @@ TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { }); SyncMetadataManager manager(metadata_path, false); - const std::string provider_type = "https://realm.example.org"; SECTION("can be properly constructed") { const auto identity = "testcase1a"; - auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type); + auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->provider_type() == provider_type); REQUIRE(user_metadata->access_token().empty()); } SECTION("properly reflects updating state") { const auto identity = "testcase1b"; const std::string sample_token = "this_is_a_user_token"; - auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type); + auto user_metadata = manager.get_or_make_user_metadata(identity); user_metadata->set_access_token(sample_token); REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->provider_type() == provider_type); REQUIRE(user_metadata->access_token() == sample_token); } SECTION("can be properly re-retrieved from the same manager") { const auto identity = "testcase1c"; const std::string sample_token = "this_is_a_user_token"; - auto first = manager.get_or_make_user_metadata(identity, provider_type); + auto first = manager.get_or_make_user_metadata(identity); first->set_access_token(sample_token); // Get a second instance of the user metadata for the same identity. - auto second = manager.get_or_make_user_metadata(identity, provider_type, false); + auto second = manager.get_or_make_user_metadata(identity, false); REQUIRE(second->identity() == identity); - REQUIRE(second->provider_type() == provider_type); REQUIRE(second->access_token() == sample_token); } SECTION("properly reflects changes across different instances") { const auto identity = "testcase1d"; const std::string sample_token_1 = "this_is_a_user_token"; - auto first = manager.get_or_make_user_metadata(identity, provider_type); - auto second = manager.get_or_make_user_metadata(identity, provider_type); + auto first = manager.get_or_make_user_metadata(identity); + auto second = manager.get_or_make_user_metadata(identity); first->set_access_token(sample_token_1); REQUIRE(first->identity() == identity); - REQUIRE(first->provider_type() == provider_type); REQUIRE(first->access_token() == sample_token_1); REQUIRE(second->identity() == identity); - REQUIRE(second->provider_type() == provider_type); REQUIRE(second->access_token() == sample_token_1); // Set the state again. const std::string sample_token_2 = "this_is_another_user_token"; second->set_access_token(sample_token_2); REQUIRE(first->identity() == identity); - REQUIRE(first->provider_type() == provider_type); REQUIRE(first->access_token() == sample_token_2); REQUIRE(second->identity() == identity); - REQUIRE(second->provider_type() == provider_type); REQUIRE(second->access_token() == sample_token_2); } SECTION("can be removed") { const auto identity = "testcase1e"; - auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type); + auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(user_metadata->is_valid()); user_metadata->remove(); REQUIRE(!user_metadata->is_valid()); @@ -115,25 +107,24 @@ TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { SECTION("with no prior metadata for the identifier") { const auto identity = "testcase1g1"; - auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type, false); + auto user_metadata = manager.get_or_make_user_metadata(identity, false); REQUIRE(!user_metadata); } SECTION("with valid prior metadata for the identifier") { const auto identity = "testcase1g2"; - auto first = manager.get_or_make_user_metadata(identity, provider_type); + auto first = manager.get_or_make_user_metadata(identity); first->set_access_token(sample_token); - auto second = manager.get_or_make_user_metadata(identity, provider_type, false); + auto second = manager.get_or_make_user_metadata(identity, false); REQUIRE(second->is_valid()); REQUIRE(second->identity() == identity); - REQUIRE(second->provider_type() == provider_type); REQUIRE(second->access_token() == sample_token); } SECTION("with invalid prior metadata for the identifier") { const auto identity = "testcase1g3"; - auto first = manager.get_or_make_user_metadata(identity, provider_type); + auto first = manager.get_or_make_user_metadata(identity); first->set_access_token(sample_token); - first->mark_for_removal(); - auto second = manager.get_or_make_user_metadata(identity, provider_type, false); + first->set_state(SyncUser::State::Removed); + auto second = manager.get_or_make_user_metadata(identity, false); REQUIRE(!second); } } @@ -150,31 +141,24 @@ TEST_CASE("sync_metadata: user metadata APIs", "[sync][metadata]") { SECTION("properly list all marked and unmarked users") { const auto identity1 = "testcase2a1"; - const auto identity2 = "testcase2a1"; // same as identity 1 - const auto identity3 = "testcase2a3"; - const std::string provider_type_1 = "https://foobar.example.org"; - const std::string provider_type_2 = "https://realm.example.org"; - const std::string provider_type_3 = "https://realm.example.org"; - auto first = manager.get_or_make_user_metadata(identity1, provider_type_1); - auto second = manager.get_or_make_user_metadata(identity2, provider_type_2); - auto third = manager.get_or_make_user_metadata(identity3, provider_type_3); + const auto identity2 = "testcase2a3"; + auto first = manager.get_or_make_user_metadata(identity1); + auto second = manager.get_or_make_user_metadata(identity1); + auto third = manager.get_or_make_user_metadata(identity2); auto unmarked_users = manager.all_unmarked_users(); - REQUIRE(unmarked_users.size() == 3); - REQUIRE(results_contains_user(unmarked_users, identity1, provider_type_1)); - REQUIRE(results_contains_user(unmarked_users, identity2, provider_type_2)); - REQUIRE(results_contains_user(unmarked_users, identity3, provider_type_3)); + REQUIRE(unmarked_users.size() == 2); + REQUIRE(results_contains_user(unmarked_users, identity1)); + REQUIRE(results_contains_user(unmarked_users, identity2)); auto marked_users = manager.all_users_marked_for_removal(); REQUIRE(marked_users.size() == 0); // Now, mark a few users for removal. - first->mark_for_removal(); - third->mark_for_removal(); + first->set_state(SyncUser::State::Removed); unmarked_users = manager.all_unmarked_users(); REQUIRE(unmarked_users.size() == 1); - REQUIRE(results_contains_user(unmarked_users, identity2, provider_type_2)); + REQUIRE(results_contains_user(unmarked_users, identity2)); marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 2); - REQUIRE(results_contains_user(marked_users, identity1, provider_type_1)); - REQUIRE(results_contains_user(marked_users, identity3, provider_type_3)); + REQUIRE(marked_users.size() == 1); + REQUIRE(results_contains_user(marked_users, identity1)); } } @@ -198,7 +182,7 @@ TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { REQUIRE(metadata.original_name() == original_name); REQUIRE(metadata.new_name() == none); REQUIRE(metadata.action() == SyncAction::BackUpThenDeleteRealm); - REQUIRE(metadata.url() == url_1); + REQUIRE(metadata.partition() == url_1); REQUIRE(metadata.user_local_uuid() == local_uuid_1); } @@ -213,7 +197,7 @@ TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { REQUIRE(metadata_1.original_name() == original_name); REQUIRE(metadata_1.new_name() == new_name_1); REQUIRE(metadata_1.action() == SyncAction::BackUpThenDeleteRealm); - REQUIRE(metadata_1.url() == url_1); + REQUIRE(metadata_1.partition() == url_1); REQUIRE(metadata_1.user_local_uuid() == local_uuid_1); manager.make_file_action_metadata(original_name, url_2, local_uuid_2, SyncAction::DeleteRealm, new_name_2); @@ -224,7 +208,7 @@ TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { REQUIRE(metadata_2.original_name() == original_name); REQUIRE(metadata_2.new_name() == new_name_2); REQUIRE(metadata_2.action() == SyncAction::DeleteRealm); - REQUIRE(metadata_1.url() == url_2); + REQUIRE(metadata_1.partition() == url_2); REQUIRE(metadata_1.user_local_uuid() == local_uuid_2); } } @@ -266,44 +250,31 @@ TEST_CASE("sync_metadata: results", "[sync][metadata]") { SyncMetadataManager manager(metadata_path, false); const auto identity1 = "testcase3a1"; - const auto identity2 = "testcase3a1"; // same as identity 1 - const auto identity3 = "testcase3a3"; - const std::string provider_type_1 = "https://realm.example.org"; - const std::string provider_type_2 = "https://foobar.example.org"; - const std::string provider_type_3 = "https://realm.example.org"; - + const auto identity2 = "testcase3a3"; SECTION("properly update as underlying items are added") { auto results = manager.all_unmarked_users(); REQUIRE(results.size() == 0); // Add users, one at a time. - auto first = manager.get_or_make_user_metadata(identity1, provider_type_1); + auto first = manager.get_or_make_user_metadata(identity1); REQUIRE(results.size() == 1); - REQUIRE(results_contains_user(results, identity1, provider_type_1)); - auto second = manager.get_or_make_user_metadata(identity2, provider_type_2); + REQUIRE(results_contains_user(results, identity1)); + auto second = manager.get_or_make_user_metadata(identity2); REQUIRE(results.size() == 2); - REQUIRE(results_contains_user(results, identity2, provider_type_2)); - auto third = manager.get_or_make_user_metadata(identity3, provider_type_3); - REQUIRE(results.size() == 3); - REQUIRE(results_contains_user(results, identity3, provider_type_3)); + REQUIRE(results_contains_user(results, identity2)); } SECTION("properly update as underlying items are removed") { auto results = manager.all_unmarked_users(); - auto first = manager.get_or_make_user_metadata(identity1, provider_type_1); - auto second = manager.get_or_make_user_metadata(identity2, provider_type_2); - auto third = manager.get_or_make_user_metadata(identity3, provider_type_3); - REQUIRE(results.size() == 3); - REQUIRE(results_contains_user(results, identity1, provider_type_1)); - REQUIRE(results_contains_user(results, identity2, provider_type_2)); - REQUIRE(results_contains_user(results, identity3, provider_type_3)); - // Remove users, one at a time. - third->remove(); + auto first = manager.get_or_make_user_metadata(identity1); + auto second = manager.get_or_make_user_metadata(identity2); REQUIRE(results.size() == 2); - REQUIRE(!results_contains_user(results, identity3, provider_type_3)); + REQUIRE(results_contains_user(results, identity1)); + REQUIRE(results_contains_user(results, identity2)); + // Remove users, one at a time. first->remove(); REQUIRE(results.size() == 1); - REQUIRE(!results_contains_user(results, identity1, provider_type_1)); + REQUIRE(!results_contains_user(results, identity1)); second->remove(); REQUIRE(results.size() == 0); } @@ -320,18 +291,16 @@ TEST_CASE("sync_metadata: persistence across metadata manager instances", "[sync const std::string provider_type = "any-type"; const std::string sample_token = "this_is_a_user_token"; SyncMetadataManager first_manager(metadata_path, false); - auto first = first_manager.get_or_make_user_metadata(identity, provider_type); + auto first = first_manager.get_or_make_user_metadata(identity); first->set_access_token(sample_token); REQUIRE(first->identity() == identity); - REQUIRE(first->provider_type() == provider_type); REQUIRE(first->access_token() == sample_token); REQUIRE(first->state() == SyncUser::State::LoggedIn); first->set_state(SyncUser::State::LoggedOut); SyncMetadataManager second_manager(metadata_path, false); - auto second = second_manager.get_or_make_user_metadata(identity, provider_type, false); + auto second = second_manager.get_or_make_user_metadata(identity, false); REQUIRE(second->identity() == identity); - REQUIRE(second->provider_type() == provider_type); REQUIRE(second->access_token() == sample_token); REQUIRE(second->state() == SyncUser::State::LoggedOut); } @@ -344,7 +313,6 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { }); const auto identity0 = "identity0"; - const auto auth_url = "https://realm.example.org"; SECTION("prohibits opening the metadata Realm with different keys") { SECTION("different keys") { { @@ -352,10 +320,9 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { std::vector key0 = make_test_encryption_key(10); SyncMetadataManager manager0(metadata_path, true, key0); - auto user_metadata0 = manager0.get_or_make_user_metadata(identity0, auth_url); + auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); REQUIRE(bool(user_metadata0)); CHECK(user_metadata0->identity() == identity0); - CHECK(user_metadata0->provider_type() == auth_url); CHECK(user_metadata0->access_token().empty()); CHECK(user_metadata0->is_valid()); } @@ -364,15 +331,14 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { std::vector key1 = make_test_encryption_key(11); SyncMetadataManager manager1(metadata_path, true, key1); - auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, auth_url, false); + auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); // Expect previous metadata to have been deleted CHECK_FALSE(bool(user_metadata1)); // But new metadata can still be created const auto identity1 = "identity1"; - auto user_metadata2 = manager1.get_or_make_user_metadata(identity1, auth_url); + auto user_metadata2 = manager1.get_or_make_user_metadata(identity1); CHECK(user_metadata2->identity() == identity1); - CHECK(user_metadata2->provider_type() == auth_url); CHECK(user_metadata2->access_token().empty()); CHECK(user_metadata2->is_valid()); } @@ -381,25 +347,23 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { // Encrypt metadata realm at path, make metadata SyncMetadataManager manager0(metadata_path, true, make_test_encryption_key(10)); - auto user_metadata0 = manager0.get_or_make_user_metadata(identity0, auth_url); + auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); REQUIRE(bool(user_metadata0)); CHECK(user_metadata0->identity() == identity0); - CHECK(user_metadata0->provider_type() == auth_url); CHECK(user_metadata0->access_token().empty()); CHECK(user_metadata0->is_valid()); } // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope // Open new metadata realm at path with different encryption configuration SyncMetadataManager manager1(metadata_path, false); - auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, auth_url, false); + auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); // Expect previous metadata to have been deleted CHECK_FALSE(bool(user_metadata1)); // But new metadata can still be created const auto identity1 = "identity1"; - auto user_metadata2 = manager1.get_or_make_user_metadata(identity1, auth_url); + auto user_metadata2 = manager1.get_or_make_user_metadata(identity1); CHECK(user_metadata2->identity() == identity1); - CHECK(user_metadata2->provider_type() == auth_url); CHECK(user_metadata2->access_token().empty()); CHECK(user_metadata2->is_valid()); } @@ -409,18 +373,16 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { std::vector key = make_test_encryption_key(10); const auto identity = "testcase5a"; SyncMetadataManager manager(metadata_path, true, key); - auto user_metadata = manager.get_or_make_user_metadata(identity, auth_url); + auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(bool(user_metadata)); CHECK(user_metadata->identity() == identity); - CHECK(user_metadata->provider_type() == auth_url); CHECK(user_metadata->access_token().empty()); CHECK(user_metadata->is_valid()); // Reopen the metadata file with the same key. SyncMetadataManager manager_2(metadata_path, true, key); - auto user_metadata_2 = manager_2.get_or_make_user_metadata(identity, auth_url, false); + auto user_metadata_2 = manager_2.get_or_make_user_metadata(identity, false); REQUIRE(bool(user_metadata_2)); CHECK(user_metadata_2->identity() == identity); - CHECK(user_metadata_2->provider_type() == auth_url); CHECK(user_metadata_2->is_valid()); } @@ -504,14 +466,80 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { const auto identity = "metadata migration test"; const std::string sample_token = "metadata migration token"; + const auto access_token_1 = encode_fake_jwt("access token 1", 456, 123); + const auto access_token_2 = encode_fake_jwt("access token 2", 456, 124); + const auto refresh_token_1 = encode_fake_jwt("refresh token 1", 456, 123); + const auto refresh_token_2 = encode_fake_jwt("refresh token 2", 456, 124); + // change to true to create a test file for the current schema version // this will only work on unix-like systems if ((false)) { +#if false // The code to generate the v4 and v5 Realms { // Create a metadata Realm with a test user SyncMetadataManager manager(metadata_path, false); auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type); user_metadata->set_access_token(sample_token); } +#elif false // The code to generate the v6 Realm + // Code to generate the v6 metadata Realm used to test the 6 -> 7 migration + { + using State = SyncUser::State; + SyncMetadataManager manager(metadata_path, false); + + auto user = manager.get_or_make_user_metadata("removed user", ""); + user->set_state(State::Removed); + + auto make_user_pair = [&](const char* name, State state1, State state2, const std::string& token_1, + const std::string& token_2) { + auto user = manager.get_or_make_user_metadata(name, "a"); + user->set_state_and_tokens(state1, token_1, refresh_token_1); + user->set_identities({{"identity 1", "a"}, {"shared identity", "shared"}}); + user->add_realm_file_path("file 1"); + user->add_realm_file_path("file 2"); + + user = manager.get_or_make_user_metadata(name, "b"); + user->set_state_and_tokens(state2, token_2, refresh_token_2); + user->set_identities({{"identity 2", "b"}, {"shared identity", "shared"}}); + user->add_realm_file_path("file 2"); + user->add_realm_file_path("file 3"); + }; + + make_user_pair("first logged in, second logged out", State::LoggedIn, State::LoggedOut, access_token_1, + access_token_2); + make_user_pair("first logged in, second removed", State::LoggedIn, State::Removed, access_token_1, + access_token_2); + make_user_pair("second logged in, first logged out", State::LoggedOut, State::LoggedIn, access_token_1, + access_token_2); + make_user_pair("second logged in, first removed", State::Removed, State::LoggedIn, access_token_1, + access_token_2); + make_user_pair("both logged in, first newer", State::LoggedIn, State::LoggedIn, access_token_2, + access_token_1); + make_user_pair("both logged in, second newer", State::LoggedIn, State::LoggedIn, access_token_1, + access_token_2); + } + + // Replace the randomly generated UUIDs with deterministic values + { + Realm::Config config; + config.path = metadata_path; + auto realm = Realm::get_shared_realm(config); + realm->begin_transaction(); + auto& group = realm->read_group(); + auto table = group.get_table("class_UserMetadata"); + auto col = table->get_column_key("local_uuid"); + size_t i = 0; + for (auto& obj : *table) { + obj.set(col, util::to_string(i++)); + } + realm->commit_transaction(); + } +#else + { // Create a metadata Realm with a test user + SyncMetadataManager manager(metadata_path, false); + auto user_metadata = manager.get_or_make_user_metadata(identity); + user_metadata->set_access_token(sample_token); + } +#endif // Open the metadata Realm directly and grab the schema version from it Realm::Config config; @@ -540,19 +568,52 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { SECTION("open schema version 4") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v4.realm", metadata_path); SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type); + auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->provider_type() == provider_type); REQUIRE(user_metadata->access_token() == sample_token); } SECTION("open schema version 5") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v5.realm", metadata_path); SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type); + auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->provider_type() == provider_type); REQUIRE(user_metadata->access_token() == sample_token); } + + SECTION("open schema version 6") { + using State = SyncUser::State; + File::copy(test_util::get_test_resource_path() + "sync-metadata-v6.realm", metadata_path); + SyncMetadataManager manager(metadata_path, false); + + SyncUserIdentity id_1{"identity 1", "a"}; + SyncUserIdentity id_2{"identity 2", "b"}; + SyncUserIdentity id_shared{"shared identity", "shared"}; + const std::vector all_ids = {id_1, id_shared, id_2}; + const std::vector realm_files = {"file 1", "file 2", "file 3"}; + + auto check_user = [&](const char* user_id, State state, const std::string& access_token, + const std::string& refresh_token, const std::vector& uuids) { + auto user = manager.get_or_make_user_metadata(user_id, false); + CAPTURE(user_id); + REQUIRE(user); + CHECK(user->state() == state); + CHECK(user->access_token() == access_token); + CHECK(user->refresh_token() == refresh_token); + CHECK(user->legacy_identities() == uuids); + CHECK(user->identities() == all_ids); + CHECK(user->realm_file_paths() == realm_files); + }; + + REQUIRE_FALSE(manager.get_or_make_user_metadata("removed user", false)); + check_user("first logged in, second logged out", State::LoggedIn, access_token_1, refresh_token_1, + {"1", "2"}); + check_user("first logged in, second removed", State::LoggedIn, access_token_1, refresh_token_1, {"3", "4"}); + check_user("second logged in, first logged out", State::LoggedIn, access_token_2, refresh_token_2, + {"5", "6"}); + check_user("second logged in, first removed", State::LoggedIn, access_token_2, refresh_token_2, {"7", "8"}); + check_user("both logged in, first newer", State::LoggedIn, access_token_2, refresh_token_1, {"9", "10"}); + check_user("both logged in, second newer", State::LoggedIn, access_token_2, refresh_token_2, {"11", "12"}); + } } #endif // SWIFT_PACKAGE diff --git a/test/object-store/sync/session/connection_change_notifications.cpp b/test/object-store/sync/session/connection_change_notifications.cpp index 4f61c0a8ae3..aabfb812494 100644 --- a/test/object-store/sync/session/connection_change_notifications.cpp +++ b/test/object-store/sync/session/connection_change_notifications.cpp @@ -41,7 +41,6 @@ using namespace realm; using namespace realm::util; -static const std::string dummy_auth_url = "https://realm.example.org"; static const std::string dummy_device_id = "123400000000000000000000"; static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_connection_state_changes"; @@ -54,9 +53,8 @@ TEST_CASE("sync: Connection state changes", "[sync][session][connection change]" config.base_path = base_path; TestSyncManager init_sync_manager(config); auto app = init_sync_manager.app(); - auto user = - app->sync_manager()->get_user("user", ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("also_not_a_real_token"), dummy_auth_url, dummy_device_id); + auto user = app->sync_manager()->get_user("user", ENCODE_FAKE_JWT("not_a_real_token"), + ENCODE_FAKE_JWT("also_not_a_real_token"), dummy_device_id); SECTION("register connection change listener") { auto session = sync_session( diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index 0697d9dfd7c..7839507ff9e 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -41,13 +41,12 @@ using namespace realm; using namespace realm::util; -static const std::string dummy_auth_url = "https://realm.example.org"; static const std::string dummy_device_id = "123400000000000000000000"; static std::shared_ptr get_user(const std::shared_ptr& app) { return app->sync_manager()->get_user("user_id", ENCODE_FAKE_JWT("fake_refresh_token"), - ENCODE_FAKE_JWT("fake_access_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("fake_access_token"), dummy_device_id); } TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { @@ -462,7 +461,6 @@ TEST_CASE("sync: error handling", "[sync][session]") { } TEST_CASE("sync: stop policy behavior", "[sync][session]") { - const std::string dummy_auth_url = "https://realm.example.org"; if (!EventLoop::has_implementation()) return; diff --git a/test/object-store/sync/session/wait_for_completion.cpp b/test/object-store/sync/session/wait_for_completion.cpp index 701c6df37b1..3ed4d639fd5 100644 --- a/test/object-store/sync/session/wait_for_completion.cpp +++ b/test/object-store/sync/session/wait_for_completion.cpp @@ -32,7 +32,6 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio if (!EventLoop::has_implementation()) return; - const std::string dummy_auth_url = "https://realm.example.org"; const std::string dummy_device_id = "123400000000000000000000"; // Disable file-related functionality and metadata functionality for testing purposes. @@ -46,7 +45,7 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio SECTION("works properly when called after the session is bound") { server.start(); auto user = sync_manager->get_user("user-async-wait-download-1", ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); auto session = sync_session(user, "/async-wait-download-1", [](auto, auto) {}); EventLoop::main().run_until([&] { return sessions_are_active(*session); @@ -64,7 +63,7 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio server.start(); const auto user_id = "user-async-wait-download-3"; auto user = sync_manager->get_user(user_id, ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); auto session = sync_session(user, "/user-async-wait-download-3", [](auto, auto) {}); EventLoop::main().run_until([&] { return sessions_are_active(*session); @@ -82,7 +81,7 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio REQUIRE(handler_called == false); // Log the user back in user = sync_manager->get_user(user_id, ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); EventLoop::main().run_until([&] { return sessions_are_active(*session); }); @@ -94,7 +93,7 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio SECTION("aborts properly when queued and the session errors out") { auto user = sync_manager->get_user("user-async-wait-download-4", ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); std::atomic error_count(0); std::shared_ptr session = sync_session(user, "/async-wait-download-4", [&](auto, auto) { ++error_count; @@ -121,7 +120,6 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] if (!EventLoop::has_implementation()) return; - const std::string dummy_auth_url = "https://realm.example.org"; const std::string dummy_device_id = "123400000000000000000000"; // Disable file-related functionality and metadata functionality for testing purposes. @@ -137,7 +135,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] SECTION("works properly when called after the session is bound") { server.start(); auto user = sync_manager->get_user("user-async-wait-upload-1", ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); auto session = sync_session(user, "/async-wait-upload-1", [](auto, auto) {}); EventLoop::main().run_until([&] { return sessions_are_active(*session); @@ -155,7 +153,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] server.start(); const auto user_id = "user-async-wait-upload-3"; auto user = sync_manager->get_user(user_id, ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); auto session = sync_session(user, "/user-async-wait-upload-3", [](auto, auto) {}); EventLoop::main().run_until([&] { return sessions_are_active(*session); @@ -173,7 +171,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] REQUIRE(handler_called == false); // Log the user back in user = sync_manager->get_user(user_id, ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); EventLoop::main().run_until([&] { return sessions_are_active(*session); }); @@ -188,7 +186,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] // SECTION("aborts properly when queued and the session errors out") { // using ProtocolError = realm::sync::ProtocolError; // auto user = SyncManager::shared().get_user("user-async-wait-upload-4", - // ENCODE_FAKE_JWT("not_a_real_token"), ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, + // ENCODE_FAKE_JWT("not_a_real_token"), ENCODE_FAKE_JWT("not_a_real_token"), // dummy_device_id); std::atomic error_count(0); std::shared_ptr session = // sync_session(user, "/async-wait-upload-4", // [&](auto e) { diff --git a/test/object-store/sync/sync_manager.cpp b/test/object-store/sync/sync_manager.cpp index fed59459897..db14354e232 100644 --- a/test/object-store/sync/sync_manager.cpp +++ b/test/object-store/sync/sync_manager.cpp @@ -39,13 +39,12 @@ static const std::string dummy_device_id = "123400000000000000000000"; namespace { bool validate_user_in_vector(std::vector> vector, const std::string& identity, - const std::string& provider_type, const std::string& refresh_token, - const std::string& access_token, const std::string& device_id) + const std::string& refresh_token, const std::string& access_token, + const std::string& device_id) { for (auto& user : vector) { if (user->identity() == identity && user->refresh_token() == refresh_token && - provider_type == user->provider_type() && user->access_token() == access_token && user->has_device_id() && - user->device_id() == device_id) { + user->access_token() == access_token && user->has_device_id() && user->device_id() == device_id) { return true; } } @@ -63,7 +62,6 @@ TEST_CASE("sync_manager: basic properties and APIs", "[sync][sync manager]") { } TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { - const std::string auth_server_url = "https://realm.example.org"; const std::string raw_url = "realms://realm.example.org/a/b/~/123456/xyz"; SECTION("should work properly without metadata") { @@ -73,8 +71,7 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { auto base_path = fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / identity; const auto expected = base_path / "realms%3A%2F%2Frealm.example.org%2Fa%2Fb%2F%7E%2F123456%2Fxyz.realm"; auto user = tsm.app()->sync_manager()->get_user(identity, ENCODE_FAKE_JWT("dummy_token"), - ENCODE_FAKE_JWT("not_a_real_token"), auth_server_url, - dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); REQUIRE(user->identity() == identity); SyncConfig config(user, bson::Bson{}); REQUIRE(tsm.app()->sync_manager()->path_for_realm(config, raw_url) == expected); @@ -89,8 +86,7 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { auto base_path = fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / identity; const auto expected = base_path / "realms%3A%2F%2Frealm.example.org%2Fa%2Fb%2F%7E%2F123456%2Fxyz.realm"; auto user = tsm.app()->sync_manager()->get_user(identity, ENCODE_FAKE_JWT("dummy_token"), - ENCODE_FAKE_JWT("not_a_real_token"), auth_server_url, - dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); REQUIRE(user->identity() == identity); SyncConfig config(user, bson::Bson{}); REQUIRE(tsm.app()->sync_manager()->path_for_realm(config, raw_url) == expected); @@ -104,8 +100,7 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { const std::string identity = random_string(10); auto base_path = fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / identity; auto user = tsm.app()->sync_manager()->get_user(identity, ENCODE_FAKE_JWT("dummy_token"), - ENCODE_FAKE_JWT("not_a_real_token"), auth_server_url, - dummy_device_id); + ENCODE_FAKE_JWT("not_a_real_token"), dummy_device_id); // Directory should not be created until we get the path REQUIRE_DIR_PATH_DOES_NOT_EXIST(base_path); @@ -184,10 +179,6 @@ TEST_CASE("sync_manager: user state management", "[sync][sync manager]") { TestSyncManager init_sync_manager(SyncManager::MetadataMode::NoEncryption); auto sync_manager = init_sync_manager.app()->sync_manager(); - const std::string url_1 = "https://realm.example.org/1/"; - const std::string url_2 = "https://realm.example.org/2/"; - const std::string url_3 = "https://realm.example.org/3/"; - const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); const std::string r_token_3 = ENCODE_FAKE_JWT("baz_token"); @@ -201,78 +192,66 @@ TEST_CASE("sync_manager: user state management", "[sync][sync manager]") { const std::string identity_3 = "user-baz"; SECTION("should get all users that are created during run time") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); - sync_manager->get_user(identity_2, r_token_2, a_token_2, url_2, dummy_device_id); + sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); + sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); auto users = sync_manager->all_users(); REQUIRE(users.size() == 2); - CHECK(validate_user_in_vector(users, identity_1, url_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, url_2, r_token_2, a_token_2, dummy_device_id)); - } - - SECTION("should be able to distinguish users based solely on URL") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_2, dummy_device_id); - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_3, dummy_device_id); - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); // existing - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, url_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_1, url_2, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_1, url_2, r_token_1, a_token_1, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); } SECTION("should be able to distinguish users based solely on user ID") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); - sync_manager->get_user(identity_2, r_token_1, a_token_1, url_1, dummy_device_id); - sync_manager->get_user(identity_3, r_token_1, a_token_1, url_1, dummy_device_id); - sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); // existing + sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); + sync_manager->get_user(identity_2, r_token_1, a_token_1, dummy_device_id); + sync_manager->get_user(identity_3, r_token_1, a_token_1, dummy_device_id); + sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); // existing auto users = sync_manager->all_users(); REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, url_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, url_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, url_1, r_token_1, a_token_1, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_2, r_token_1, a_token_1, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_3, r_token_1, a_token_1, dummy_device_id)); } SECTION("should properly update state in response to users logging in and out") { auto r_token_3a = ENCODE_FAKE_JWT("qwerty"); auto a_token_3a = ENCODE_FAKE_JWT("ytrewq"); - auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); - auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, url_2, dummy_device_id); - auto u3 = sync_manager->get_user(identity_3, r_token_3, a_token_3, url_3, dummy_device_id); + auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); + auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); + auto u3 = sync_manager->get_user(identity_3, r_token_3, a_token_3, dummy_device_id); auto users = sync_manager->all_users(); REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, url_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, url_2, r_token_2, a_token_2, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, url_3, r_token_3, a_token_3, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); // Log out users 1 and 3 u1->log_out(); u3->log_out(); users = sync_manager->all_users(); REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_2, url_2, r_token_2, a_token_2, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); // Log user 3 back in - u3 = sync_manager->get_user(identity_3, r_token_3a, a_token_3a, url_3, dummy_device_id); + u3 = sync_manager->get_user(identity_3, r_token_3a, a_token_3a, dummy_device_id); users = sync_manager->all_users(); REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_2, url_2, r_token_2, a_token_2, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, url_3, r_token_3a, a_token_3a, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_3, r_token_3a, a_token_3a, dummy_device_id)); // Log user 2 out u2->log_out(); users = sync_manager->all_users(); REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_3, url_3, r_token_3a, a_token_3a, dummy_device_id)); + CHECK(validate_user_in_vector(users, identity_3, r_token_3a, a_token_3a, dummy_device_id)); } SECTION("should return current user that was created during run time") { auto u_null = sync_manager->get_current_user(); REQUIRE(u_null == nullptr); - auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, url_1, dummy_device_id); + auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); auto u_current = sync_manager->get_current_user(); REQUIRE(u_current == u1); - auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, url_2, dummy_device_id); + auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); // The current user has switched to return the most recently used: "u2" u_current = sync_manager->get_current_user(); REQUIRE(u_current == u2); @@ -290,9 +269,6 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager // Open the metadata separately, so we can investigate it ourselves. SyncMetadataManager manager(file_manager.metadata_path(), false); - const std::string url_1 = "https://realm.example.org/1/"; - const std::string url_2 = "https://realm.example.org/2/"; - const std::string url_3 = "https://realm.example.org/3/"; const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); const std::string r_token_3 = ENCODE_FAKE_JWT("baz_token"); @@ -305,20 +281,20 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager const std::string identity_2 = "bar-1"; const std::string identity_3 = "baz-1"; // First, create a few users and add them to the metadata. - auto u1 = manager.get_or_make_user_metadata(identity_1, url_1); + auto u1 = manager.get_or_make_user_metadata(identity_1); u1->set_access_token(a_token_1); u1->set_refresh_token(r_token_1); u1->set_device_id(dummy_device_id); - auto u2 = manager.get_or_make_user_metadata(identity_2, url_2); + auto u2 = manager.get_or_make_user_metadata(identity_2); u2->set_access_token(a_token_2); u2->set_refresh_token(r_token_2); u2->set_device_id(dummy_device_id); - auto u3 = manager.get_or_make_user_metadata(identity_3, url_3); + auto u3 = manager.get_or_make_user_metadata(identity_3); u3->set_access_token(a_token_3); u3->set_refresh_token(r_token_3); u3->set_device_id(dummy_device_id); // The fourth user is an "invalid" user: no token, so shouldn't show up. - auto u_invalid = manager.get_or_make_user_metadata("invalid_user", url_1); + auto u_invalid = manager.get_or_make_user_metadata("invalid_user"); REQUIRE(manager.all_unmarked_users().size() == 4); SECTION("they should be added to the active users list when metadata is enabled") { @@ -326,9 +302,9 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager TestSyncManager tsm(config); auto users = tsm.app()->sync_manager()->all_users(); REQUIRE(users.size() == 3); - REQUIRE(validate_user_in_vector(users, identity_1, url_1, r_token_1, a_token_1, dummy_device_id)); - REQUIRE(validate_user_in_vector(users, identity_2, url_2, r_token_2, a_token_2, dummy_device_id)); - REQUIRE(validate_user_in_vector(users, identity_3, url_3, r_token_3, a_token_3, dummy_device_id)); + REQUIRE(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); + REQUIRE(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); + REQUIRE(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); } SECTION("they should not be added to the active users list when metadata is disabled") { @@ -348,20 +324,23 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager std::vector paths_under_test; SECTION("when users are marked") { - const std::string provider_type = "user-pass"; const std::string identity_1 = "foo-2"; const std::string identity_2 = "bar-2"; const std::string identity_3 = "baz-2"; // Create the user metadata. - auto u1 = manager.get_or_make_user_metadata(identity_1, provider_type); - auto u2 = manager.get_or_make_user_metadata(identity_2, provider_type); + auto u1 = manager.get_or_make_user_metadata(identity_1); + auto u2 = manager.get_or_make_user_metadata(identity_2); // Don't mark this user for deletion. - auto u3 = manager.get_or_make_user_metadata(identity_3, provider_type); + auto u3 = manager.get_or_make_user_metadata(identity_3); + + u1->set_legacy_identities({"legacy1"}); + u2->set_legacy_identities({"legacy2"}); + u3->set_legacy_identities({"legacy3"}); { auto expected_u1_path = [&](const bson::Bson& partition) { - return ExpectedRealmPaths(tsm.base_file_path(), app_id, u1->identity(), u1->local_uuid(), + return ExpectedRealmPaths(tsm.base_file_path(), app_id, u1->identity(), u1->legacy_identities(), partition.to_string()); }; bson::Bson partition = "partition1"; @@ -394,12 +373,9 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager auto sync_manager = tsm.app()->sync_manager(); // Pre-populate the user directories. - auto user1 = - sync_manager->get_user(u1->identity(), r_token_1, a_token_1, u1->provider_type(), dummy_device_id); - auto user2 = - sync_manager->get_user(u2->identity(), r_token_2, a_token_2, u2->provider_type(), dummy_device_id); - auto user3 = - sync_manager->get_user(u3->identity(), r_token_3, a_token_3, u3->provider_type(), dummy_device_id); + auto user1 = sync_manager->get_user(u1->identity(), r_token_1, a_token_1, dummy_device_id); + auto user2 = sync_manager->get_user(u2->identity(), r_token_2, a_token_2, dummy_device_id); + auto user3 = sync_manager->get_user(u3->identity(), r_token_3, a_token_3, dummy_device_id); for (auto& dir : dirs_to_create) { try_make_dir(dir); } @@ -437,7 +413,7 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager TestSyncManager tsm(config); auto users = tsm.app()->sync_manager()->all_users(); REQUIRE(users.size() == 1); - REQUIRE(validate_user_in_vector(users, identity_3, provider_type, r_token_3, a_token_3, dummy_device_id)); + REQUIRE(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); REQUIRE_REALM_DOES_NOT_EXIST(paths[0]); REQUIRE_REALM_DOES_NOT_EXIST(paths[1]); REQUIRE_REALM_DOES_NOT_EXIST(paths[2]); @@ -482,16 +458,13 @@ TEST_CASE("sync_manager: file actions", "[sync][sync manager]") { const std::string uuid_3 = "uuid-baz-1"; const std::string uuid_4 = "uuid-baz-2"; - const std::string local_uuid_1 = "foo-1"; - const std::string local_uuid_2 = "bar-1"; - const std::string local_uuid_3 = "baz-1"; - const std::string local_uuid_4 = "baz-2"; + const std::vector legacy_identities; // Realm paths - const std::string realm_path_1 = file_manager.realm_file_path(uuid_1, local_uuid_1, realm_url, partition); - const std::string realm_path_2 = file_manager.realm_file_path(uuid_2, local_uuid_2, realm_url, partition); - const std::string realm_path_3 = file_manager.realm_file_path(uuid_3, local_uuid_3, realm_url, partition); - const std::string realm_path_4 = file_manager.realm_file_path(uuid_4, local_uuid_4, realm_url, partition); + const std::string realm_path_1 = file_manager.realm_file_path(uuid_1, legacy_identities, realm_url, partition); + const std::string realm_path_2 = file_manager.realm_file_path(uuid_2, legacy_identities, realm_url, partition); + const std::string realm_path_3 = file_manager.realm_file_path(uuid_3, legacy_identities, realm_url, partition); + const std::string realm_path_4 = file_manager.realm_file_path(uuid_4, legacy_identities, realm_url, partition); // On windows you can't delete a realm if the file is open elsewhere. #ifdef _WIN32 @@ -591,7 +564,7 @@ TEST_CASE("sync_manager: file actions", "[sync][sync manager]") { SECTION("should copy the Realm to the recovery_directory_path") { const std::string identity = "b241922032489d4836ecd0c82d0445f0"; - const auto realm_base_path = file_manager.realm_file_path(identity, "", "realmtasks", partition); + const auto realm_base_path = file_manager.realm_file_path(identity, {}, "realmtasks", partition); std::string recovery_path = util::reserve_unique_file_name( file_manager.recovery_directory_path(), util::create_timestamped_template("recovered_realm")); create_dummy_realm(realm_base_path); @@ -738,9 +711,9 @@ TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { sync_manager->set_session_multiplexing(sync_multiplexing_allowed); auto user_1 = sync_manager->get_user("user-name-1", ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("samesies"), "https://realm.example.org", dummy_device_id); + ENCODE_FAKE_JWT("samesies"), dummy_device_id); auto user_2 = sync_manager->get_user("user-name-2", ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("samesies"), "https://realm.example.org", dummy_device_id); + ENCODE_FAKE_JWT("samesies"), dummy_device_id); SyncTestFile file_1(user_1, "partition1", util::none); SyncTestFile file_2(user_1, "partition2", util::none); @@ -784,7 +757,7 @@ TEST_CASE("sync_manager: has_existing_sessions", "[sync][sync manager][active se std::atomic error_handler_invoked(false); Realm::Config config; auto user = sync_manager->get_user("user-name", ENCODE_FAKE_JWT("not_a_real_token"), ENCODE_FAKE_JWT("samesies"), - "https://realm.example.org", dummy_device_id); + dummy_device_id); auto create_session = [&](SyncSessionStopPolicy stop_policy) { std::shared_ptr session = sync_session( user, "/test-dying-state", diff --git a/test/object-store/sync/user.cpp b/test/object-store/sync/user.cpp index d14e48f33bc..f4643789bdd 100644 --- a/test/object-store/sync/user.cpp +++ b/test/object-store/sync/user.cpp @@ -40,14 +40,12 @@ TEST_CASE("sync_user: SyncManager `get_user()` API", "[sync][user]") { const std::string identity = "sync_test_identity"; const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - const std::string server_url = "https://realm.example.org"; SECTION("properly creates a new normal user") { - auto user = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); REQUIRE(user); // The expected state for a newly created user: REQUIRE(user->identity() == identity); - REQUIRE(user->provider_type() == server_url); REQUIRE(user->refresh_token() == refresh_token); REQUIRE(user->access_token() == access_token); REQUIRE(user->state() == SyncUser::State::LoggedIn); @@ -57,13 +55,12 @@ TEST_CASE("sync_user: SyncManager `get_user()` API", "[sync][user]") { const std::string second_refresh_token = ENCODE_FAKE_JWT("0987654321-fake-refresh-token"); const std::string second_access_token = ENCODE_FAKE_JWT("0987654321-fake-access-token"); - auto first = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); REQUIRE(first); REQUIRE(first->identity() == identity); REQUIRE(first->refresh_token() == refresh_token); // Get the user again, but with a different token. - auto second = - sync_manager->get_user(identity, second_refresh_token, second_access_token, server_url, dummy_device_id); + auto second = sync_manager->get_user(identity, second_refresh_token, second_access_token, dummy_device_id); REQUIRE(second == first); REQUIRE(second->identity() == identity); REQUIRE(second->access_token() == second_access_token); @@ -74,13 +71,12 @@ TEST_CASE("sync_user: SyncManager `get_user()` API", "[sync][user]") { const std::string second_refresh_token = ENCODE_FAKE_JWT("0987654321-fake-refresh-token"); const std::string second_access_token = ENCODE_FAKE_JWT("0987654321-fake-access-token"); - auto first = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); REQUIRE(first->identity() == identity); first->log_out(); REQUIRE(first->state() == SyncUser::State::LoggedOut); // Get the user again, with a new token. - auto second = - sync_manager->get_user(identity, second_refresh_token, second_access_token, server_url, dummy_device_id); + auto second = sync_manager->get_user(identity, second_refresh_token, second_access_token, dummy_device_id); REQUIRE(second == first); REQUIRE(second->identity() == identity); REQUIRE(second->refresh_token() == second_refresh_token); @@ -94,27 +90,26 @@ TEST_CASE("sync_user: update state and tokens", "[sync][user]") { const std::string identity = "sync_test_identity"; const std::string refresh_token = ENCODE_FAKE_JWT("fake-refresh-token-1"); const std::string access_token = ENCODE_FAKE_JWT("fake-access-token-1"); - const std::string server_url = "https://realm.example.org"; const std::string second_refresh_token = ENCODE_FAKE_JWT("fake-refresh-token-4"); const std::string second_access_token = ENCODE_FAKE_JWT("fake-access-token-4"); - auto user = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); REQUIRE(user->is_logged_in()); REQUIRE(user->refresh_token() == refresh_token); - user->update_state_and_tokens(SyncUser::State::LoggedIn, second_access_token, second_refresh_token); + user->log_in(second_access_token, second_refresh_token); REQUIRE(user->is_logged_in()); REQUIRE(user->refresh_token() == second_refresh_token); - user->update_state_and_tokens(SyncUser::State::LoggedOut, "", ""); + user->log_out(); REQUIRE(!user->is_logged_in()); REQUIRE(user->refresh_token().empty()); - user->update_state_and_tokens(SyncUser::State::LoggedIn, access_token, refresh_token); + user->log_in(access_token, refresh_token); REQUIRE(user->is_logged_in()); REQUIRE(user->refresh_token() == refresh_token); - sync_manager->remove_user(identity); + user->invalidate(); } TEST_CASE("sync_user: SyncManager `get_existing_logged_in_user()` API", "[sync][user]") { @@ -123,7 +118,6 @@ TEST_CASE("sync_user: SyncManager `get_existing_logged_in_user()` API", "[sync][ const std::string identity = "sync_test_identity"; const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - const std::string server_url = "https://realm.example.org"; SECTION("properly returns a null pointer when called for a non-existent user") { std::shared_ptr user = sync_manager->get_existing_logged_in_user(identity); @@ -131,7 +125,7 @@ TEST_CASE("sync_user: SyncManager `get_existing_logged_in_user()` API", "[sync][ } SECTION("properly returns an existing logged-in user") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); REQUIRE(first->identity() == identity); REQUIRE(first->state() == SyncUser::State::LoggedIn); REQUIRE(first->device_id() == dummy_device_id); @@ -142,7 +136,7 @@ TEST_CASE("sync_user: SyncManager `get_existing_logged_in_user()` API", "[sync][ } SECTION("properly returns a null pointer for a logged-out user") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); first->log_out(); REQUIRE(first->identity() == identity); REQUIRE(first->state() == SyncUser::State::LoggedOut); @@ -158,10 +152,9 @@ TEST_CASE("sync_user: logout", "[sync][user]") { const std::string identity = "sync_test_identity"; const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - const std::string server_url = "https://realm.example.org"; SECTION("properly changes the state of the user object") { - auto user = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); REQUIRE(user->state() == SyncUser::State::LoggedIn); user->log_out(); REQUIRE(user->state() == SyncUser::State::LoggedOut); @@ -179,15 +172,13 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_1"; const std::string refresh_token = ENCODE_FAKE_JWT("r-token-1"); const std::string access_token = ENCODE_FAKE_JWT("a-token-1"); - const std::string server_url = "https://realm.example.org/1/"; const std::vector identities{{"12345", "test_case_provider"}}; - auto user = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); - user->update_identities(identities); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); + user->update_user_profile(identities, {}); // Now try to pull the user out of the shadow manager directly. - auto metadata = manager.get_or_make_user_metadata(identity, server_url, false); + auto metadata = manager.get_or_make_user_metadata(identity, false); REQUIRE((bool)metadata); REQUIRE(metadata->is_valid()); - REQUIRE(metadata->provider_type() == server_url); REQUIRE(metadata->access_token() == access_token); REQUIRE(metadata->refresh_token() == refresh_token); REQUIRE(metadata->device_id() == dummy_device_id); @@ -198,16 +189,14 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_1"; const std::string refresh_token = ENCODE_FAKE_JWT("r-token-1"); const std::string access_token = ENCODE_FAKE_JWT("a-token-1"); - const std::string server_url = "https://realm.example.org/1/"; const std::vector identities{{"12345", "test_case_provider"}}; - auto user = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); - user->update_identities(identities); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); + user->update_user_profile(identities, {}); user->log_out(); // Now try to pull the user out of the shadow manager directly. - auto metadata = manager.get_or_make_user_metadata(identity, server_url, false); + auto metadata = manager.get_or_make_user_metadata(identity, false); REQUIRE((bool)metadata); REQUIRE(metadata->is_valid()); - REQUIRE(metadata->provider_type() == server_url); REQUIRE(metadata->access_token() == ""); REQUIRE(metadata->refresh_token() == ""); REQUIRE(metadata->device_id() == dummy_device_id); @@ -220,16 +209,15 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_2"; const std::string refresh_token = ENCODE_FAKE_JWT("r_token-2a"); const std::string access_token = ENCODE_FAKE_JWT("a_token-1a"); - const std::string server_url = "https://realm.example.org/2/"; // Create the user and validate it. - auto first = sync_manager->get_user(identity, refresh_token, access_token, server_url, dummy_device_id); - auto first_metadata = manager.get_or_make_user_metadata(identity, server_url, false); + auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); + auto first_metadata = manager.get_or_make_user_metadata(identity, false); REQUIRE(first_metadata->is_valid()); REQUIRE(first_metadata->access_token() == access_token); const std::string token_2 = ENCODE_FAKE_JWT("token-2b"); // Update the user. - auto second = sync_manager->get_user(identity, refresh_token, token_2, server_url, dummy_device_id); - auto second_metadata = manager.get_or_make_user_metadata(identity, server_url, false); + auto second = sync_manager->get_user(identity, refresh_token, token_2, dummy_device_id); + auto second_metadata = manager.get_or_make_user_metadata(identity, false); REQUIRE(second_metadata->is_valid()); REQUIRE(second_metadata->access_token() == token_2); } @@ -238,9 +226,8 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_3"; const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - const std::string provider_type = app::IdentityProviderGoogle; // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, provider_type, dummy_device_id); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); auto marked_users = manager.all_users_marked_for_removal(); REQUIRE(marked_users.size() == 0); // Log out the user. @@ -253,9 +240,9 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_3"; const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - const std::string provider_type = app::IdentityProviderAnonymous; // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, provider_type, dummy_device_id); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); + user->update_user_profile({{"id", app::IdentityProviderAnonymous}}, {}); auto marked_users = manager.all_users_marked_for_removal(); REQUIRE(marked_users.size() == 0); // Log out the user. @@ -267,16 +254,15 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_3"; const std::string refresh_token = ENCODE_FAKE_JWT("r-token-4a"); const std::string access_token = ENCODE_FAKE_JWT("a-token-4a"); - const std::string provider_type = app::IdentityProviderApple; // Create the user and log it out. - auto first = sync_manager->get_user(identity, refresh_token, access_token, provider_type, dummy_device_id); + auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); first->log_out(); REQUIRE(sync_manager->all_users().size() == 1); REQUIRE(sync_manager->all_users()[0]->state() == SyncUser::State::LoggedOut); // Log the user back in. const std::string r_token_2 = ENCODE_FAKE_JWT("r-token-4b"); const std::string a_token_2 = ENCODE_FAKE_JWT("atoken-4b"); - auto second = sync_manager->get_user(identity, r_token_2, a_token_2, provider_type, dummy_device_id); + auto second = sync_manager->get_user(identity, r_token_2, a_token_2, dummy_device_id); REQUIRE(sync_manager->all_users().size() == 1); REQUIRE(sync_manager->all_users()[0]->state() == SyncUser::State::LoggedIn); } @@ -285,9 +271,8 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { const std::string identity = "test_identity_3"; const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - const std::string provider_type = app::IdentityProviderAnonymous; // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, provider_type, dummy_device_id); + auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); sync_manager->set_current_user(identity); REQUIRE(sync_manager->get_current_user() == user); REQUIRE(sync_manager->all_users().size() == 1); diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index 416c09d9dc1..d2ba5c35f5b 100644 --- a/test/object-store/util/sync/sync_test_utils.cpp +++ b/test/object-store/util/sync/sync_test_utils.cpp @@ -52,12 +52,11 @@ std::ostream& operator<<(std::ostream& os, util::Optional error) return os; } -bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity, - const std::string& provider_type) +bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity) { for (size_t i = 0; i < results.size(); i++) { auto this_result = results.get(i); - if (this_result.identity() == identity && this_result.provider_type() == provider_type) { + if (this_result.identity() == identity) { return true; } } @@ -122,7 +121,7 @@ auto do_hash = [](const std::string& name) -> std::string { }; ExpectedRealmPaths::ExpectedRealmPaths(const std::string& base_path, const std::string& app_id, - const std::string& identity, const std::string& local_identity, + const std::string& identity, const std::vector& legacy_identities, const std::string& partition) { // This is copied from SyncManager.cpp string_from_partition() in order to prevent @@ -158,6 +157,10 @@ ExpectedRealmPaths::ExpectedRealmPaths(const std::string& base_path, const std:: const auto preferred_name = manager_path / identity / clean_name; current_preferred_path = preferred_name.string() + ".realm"; fallback_hashed_path = (manager_path / do_hash(preferred_name.string())).string() + ".realm"; + + if (legacy_identities.size() < 1) + return; + auto& local_identity = legacy_identities[0]; legacy_sync_directories_to_make.push_back((manager_path / local_identity).string()); std::string encoded_partition = util::make_percent_encoded_string(partition); legacy_local_id_path = (manager_path / local_identity / encoded_partition).concat(".realm").string(); diff --git a/test/object-store/util/sync/sync_test_utils.hpp b/test/object-store/util/sync/sync_test_utils.hpp index f1a60f5ab1f..4a81eca8677 100644 --- a/test/object-store/util/sync/sync_test_utils.hpp +++ b/test/object-store/util/sync/sync_test_utils.hpp @@ -46,8 +46,7 @@ namespace realm { -bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity, - const std::string& auth_server); +bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity); bool results_contains_original_name(SyncFileActionMetadataResults& results, const std::string& original_name); void timed_wait_for(util::FunctionRef condition, @@ -119,7 +118,7 @@ util::Future wait_for_future(util::Future&& input, std::chrono::millisecon struct ExpectedRealmPaths { ExpectedRealmPaths(const std::string& base_path, const std::string& app_id, const std::string& user_identity, - const std::string& local_identity, const std::string& partition); + const std::vector& legacy_identities, const std::string& partition); std::string current_preferred_path; std::string fallback_hashed_path; std::string legacy_local_id_path; diff --git a/test/object-store/util/test_file.cpp b/test/object-store/util/test_file.cpp index 2e518b8b9d3..c023100c360 100644 --- a/test/object-store/util/test_file.cpp +++ b/test/object-store/util/test_file.cpp @@ -131,8 +131,7 @@ static const std::string fake_device_id = "123400000000000000000000"; static std::shared_ptr get_fake_user(app::App& app, const std::string& user_name) { - return app.sync_manager()->get_user(user_name, fake_refresh_token, fake_access_token, app.base_url(), - fake_device_id); + return app.sync_manager()->get_user(user_name, fake_refresh_token, fake_access_token, fake_device_id); } SyncTestFile::SyncTestFile(std::shared_ptr app, std::string name, std::string user_name) diff --git a/test/object-store/util/unit_test_transport.cpp b/test/object-store/util/unit_test_transport.cpp new file mode 100644 index 00000000000..ee3197351f7 --- /dev/null +++ b/test/object-store/util/unit_test_transport.cpp @@ -0,0 +1,228 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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/unit_test_transport.hpp" + +#include +#include +#include +#include + +#include + +using namespace realm; +using namespace realm::app; + +std::string UnitTestTransport::access_token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2RldklkIjoi" + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU0M2Rk" + "Y2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s"; +const std::string UnitTestTransport::api_key = "lVRPQVYBJSIbGos2ZZn0mGaIq1SIOsGaZ5lrcp8bxlR5jg4OGuGwQq1GkektNQ3i"; +const std::string UnitTestTransport::api_key_id = "5e5e6f0abe4ae2a2c2c2d329"; +const std::string UnitTestTransport::api_key_name = "some_api_key_name"; +const std::string UnitTestTransport::auth_route = "https://mongodb.com/unittests"; +const std::string UnitTestTransport::identity_0_id = "Ursus arctos isabellinus"; +const std::string UnitTestTransport::identity_1_id = "Ursus arctos horribilis"; + +UnitTestTransport::UnitTestTransport(const std::string& provider_type, uint64_t request_timeout) + : m_provider_type(provider_type) + , m_request_timeout(request_timeout) + , m_options({{"device", + {{"appId", "app_id"}, + {"platform", util::get_library_platform()}, + {"platformVersion", "Object Store Test Platform Version"}, + {"sdk", "SDK Name"}, + {"sdkVersion", "SDK Version"}, + {"cpuArch", util::get_library_cpu_arch()}, + {"deviceName", "Device Name"}, + {"deviceVersion", "Device Version"}, + {"frameworkName", "Framework Name"}, + {"frameworkVersion", "Framework Version"}, + {"coreVersion", REALM_VERSION_STRING}, + {"bundleId", "Bundle Id"}}}}) +{ +} + +void UnitTestTransport::handle_profile(const Request& request, + util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::get); + auto content_type = AppUtils::find_header("Content-Type", request.headers); + CHECK(content_type); + CHECK(content_type->second == "application/json;charset=utf-8"); + auto authorization = AppUtils::find_header("Authorization", request.headers); + CHECK(authorization); + CHECK(authorization->second == "Bearer " + access_token); + CHECK(request.body.empty()); + CHECK(request.timeout_ms == m_request_timeout); + + std::string user_id = util::uuid_string(); + std::string response; + if (m_provider_type == IdentityProviderAnonymous) { + response = nlohmann::json({{"user_id", user_id}, + {"identities", {{{"id", identity_0_id}, {"provider_type", m_provider_type}}}}, + {"data", m_user_profile}}) + .dump(); + } + else { + response = nlohmann::json({{"user_id", user_id}, + {"identities", + {{{"id", identity_0_id}, {"provider_type", m_provider_type}}, + {{"id", identity_1_id}, {"provider_type", "lol_wut"}}}}, + {"data", m_user_profile}}) + .dump(); + } + + completion(Response{200, 0, {}, response}); +} + +void UnitTestTransport::handle_login(const Request& request, util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::post); + auto item = AppUtils::find_header("Content-Type", request.headers); + CHECK(item); + CHECK(item->second == "application/json;charset=utf-8"); + CHECK(nlohmann::json::parse(request.body)["options"] == m_options); + + CHECK(request.timeout_ms == m_request_timeout); + + std::string response = nlohmann::json({{"access_token", access_token}, + {"refresh_token", access_token}, + {"user_id", util::uuid_string()}, + {"device_id", "Panda Bear"}}) + .dump(); + + completion(Response{200, 0, {}, response}); +} + +void UnitTestTransport::handle_location(const Request& request, + util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::get); + CHECK(request.timeout_ms == m_request_timeout); + + std::string response = nlohmann::json({{"deployment_model", "this"}, + {"hostname", "field"}, + {"ws_hostname", "shouldn't"}, + {"location", "matter"}}) + .dump(); + + completion(Response{200, 0, {}, response}); +} + +void UnitTestTransport::handle_create_api_key(const Request& request, + util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::post); + auto item = AppUtils::find_header("Content-Type", request.headers); + CHECK(item); + CHECK(item->second == "application/json;charset=utf-8"); + CHECK(nlohmann::json::parse(request.body) == nlohmann::json({{"name", api_key_name}})); + CHECK(request.timeout_ms == m_request_timeout); + + std::string response = + nlohmann::json({{"_id", api_key_id}, {"key", api_key}, {"name", api_key_name}, {"disabled", false}}).dump(); + + completion(Response{200, 0, {}, response}); +} + +void UnitTestTransport::handle_fetch_api_key(const Request& request, + util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::get); + auto item = AppUtils::find_header("Content-Type", request.headers); + CHECK(item); + CHECK(item->second == "application/json;charset=utf-8"); + + CHECK(request.body == ""); + CHECK(request.timeout_ms == m_request_timeout); + + std::string response = nlohmann::json({{"_id", api_key_id}, {"name", api_key_name}, {"disabled", false}}).dump(); + + completion(Response{200, 0, {}, response}); +} + +void UnitTestTransport::handle_fetch_api_keys(const Request& request, + util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::get); + auto item = AppUtils::find_header("Content-Type", request.headers); + CHECK(item); + CHECK(item->second == "application/json;charset=utf-8"); + + CHECK(request.body == ""); + CHECK(request.timeout_ms == m_request_timeout); + + auto elements = std::vector(); + for (int i = 0; i < 2; i++) { + elements.push_back({{"_id", api_key_id}, {"name", api_key_name}, {"disabled", false}}); + } + + completion(Response{200, 0, {}, nlohmann::json(elements).dump()}); +} + +void UnitTestTransport::handle_token_refresh(const Request& request, + util::UniqueFunction&& completion) +{ + CHECK(request.method == HttpMethod::post); + auto item = AppUtils::find_header("Content-Type", request.headers); + CHECK(item); + CHECK(item->second == "application/json;charset=utf-8"); + + CHECK(request.body == ""); + CHECK(request.timeout_ms == m_request_timeout); + + auto elements = std::vector(); + nlohmann::json json{{"access_token", access_token}}; + + completion(Response{200, 0, {}, json.dump()}); +} + +void UnitTestTransport::send_request_to_server(const Request& request, + util::UniqueFunction&& completion) +{ + if (request.url.find("/login") != std::string::npos) { + handle_login(request, std::move(completion)); + } + else if (request.url.find("/profile") != std::string::npos) { + handle_profile(request, std::move(completion)); + } + else if (request.url.find("/session") != std::string::npos && request.method != HttpMethod::post) { + completion(Response{200, 0, {}, ""}); + } + else if (request.url.find("/api_keys") != std::string::npos && request.method == HttpMethod::post) { + handle_create_api_key(request, std::move(completion)); + } + else if (request.url.find(util::format("/api_keys/%1", api_key_id)) != std::string::npos && + request.method == HttpMethod::get) { + handle_fetch_api_key(request, std::move(completion)); + } + else if (request.url.find("/api_keys") != std::string::npos && request.method == HttpMethod::get) { + handle_fetch_api_keys(request, std::move(completion)); + } + else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { + handle_token_refresh(request, std::move(completion)); + } + else if (request.url.find("/location") != std::string::npos && request.method == HttpMethod::get) { + handle_location(request, std::move(completion)); + } + else { + completion(Response{200, 0, {}, "something arbitrary"}); + } +} diff --git a/test/object-store/util/unit_test_transport.hpp b/test/object-store/util/unit_test_transport.hpp new file mode 100644 index 00000000000..9b29525ceaf --- /dev/null +++ b/test/object-store/util/unit_test_transport.hpp @@ -0,0 +1,83 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 + +#include + +class UnitTestTransport : public realm::app::GenericNetworkTransport { +public: + UnitTestTransport(const std::string& provider_type, uint64_t request_timeout); + + explicit UnitTestTransport(const std::string& provider_type = "anon-user") + : UnitTestTransport(provider_type, 60000) + { + } + explicit UnitTestTransport(uint64_t request_timeout) + : UnitTestTransport("anon-user", request_timeout) + { + } + + static std::string access_token; + + static const std::string api_key; + static const std::string api_key_id; + static const std::string api_key_name; + static const std::string auth_route; + static const std::string identity_0_id; + static const std::string identity_1_id; + + void set_provider_type(const std::string& provider_type) + { + m_provider_type = provider_type; + } + + void set_profile(nlohmann::json profile) + { + m_user_profile = std::move(profile); + } + + void set_expected_options(nlohmann::json options) + { + m_options = std::move(options); + } + + void send_request_to_server(const realm::app::Request& request, + realm::util::UniqueFunction&& completion) override; + +private: + std::string m_provider_type; + uint64_t m_request_timeout = 60000; + nlohmann::json m_user_profile = nlohmann::json::object(); + nlohmann::json m_options; + + void handle_profile(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); + void handle_login(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); + void handle_location(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); + void handle_create_api_key(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); + void handle_fetch_api_key(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); + void handle_fetch_api_keys(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); + void handle_token_refresh(const realm::app::Request& request, + realm::util::UniqueFunction&& completion); +};