From 2a65cd1d9fbb9e5bb4cb163603143f85534de02c Mon Sep 17 00:00:00 2001 From: wuandy Date: Tue, 16 Jun 2020 20:07:54 -0700 Subject: [PATCH 001/109] Use Executor::CreateSerial PiperOrigin-RevId: 316809187 --- firestore/src/common/settings_ios.mm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firestore/src/common/settings_ios.mm b/firestore/src/common/settings_ios.mm index 71f9609e0b..016683f1ec 100644 --- a/firestore/src/common/settings_ios.mm +++ b/firestore/src/common/settings_ios.mm @@ -19,8 +19,7 @@ Settings::Settings() : host_(kDefaultHost), - executor_(absl::make_unique(dispatch_queue_create( - "com.google.firebase.firestore.callback", DISPATCH_QUEUE_SERIAL))) {} + executor_(Executor::CreateSerial("com.google.firebase.firestore.callback")) {} std::unique_ptr Settings::CreateExecutor() const { return absl::make_unique(dispatch_queue()); From 6b100bc622321d4ffc5642b345fb06db5cfe638f Mon Sep 17 00:00:00 2001 From: cynthiajiang Date: Tue, 16 Jun 2020 20:10:32 -0700 Subject: [PATCH 002/109] Fix future crash that cause unity editor to crash second time enter play mode. FutureProxyManager still grabs FutureHandle that supposed to be cleaned up and released. Force release them during proxy manager's destructor. PiperOrigin-RevId: 316809477 --- app/src/reference_counted_future_impl.cc | 48 +++++++++++++++++++++--- app/src/reference_counted_future_impl.h | 3 ++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/app/src/reference_counted_future_impl.cc b/app/src/reference_counted_future_impl.cc index 94a8ca6ad1..0ad53f05cb 100644 --- a/app/src/reference_counted_future_impl.cc +++ b/app/src/reference_counted_future_impl.cc @@ -73,7 +73,17 @@ class FutureProxyManager { const FutureHandle& subject) : api_(api), subject_(subject) {} + ~FutureProxyManager() { + MutexLock lock(mutex_); + for (FutureHandle& h : clients_) { + api_->ForceReleaseFuture(h); + h = ReferenceCountedFutureImpl::kInvalidHandle; + } + clients_.clear(); + } + void RegisterClient(const FutureHandle& handle) { + MutexLock lock(mutex_); // We create one reference per client to the Future. // This way the ReferenceCountedFutureImpl will do the right thing if one // thread tries to unregister the last client while adding a new one. @@ -89,12 +99,18 @@ class FutureProxyManager { }; static void UnregisterCallback(void* data) { + if (data == nullptr) { + return; + } UnregisterData* udata = static_cast(data); - udata->proxy->UnregisterClient(udata->handle); - delete udata; + if (udata != nullptr) { + udata->proxy->UnregisterClient(udata->handle); + delete udata; + } } void UnregisterClient(const FutureHandle& handle) { + MutexLock lock(mutex_); for (FutureHandle& h : clients_) { if (h == handle) { h = ReferenceCountedFutureImpl::kInvalidHandle; @@ -108,6 +124,7 @@ class FutureProxyManager { } void CompleteClients(int error, const char* error_msg) { + MutexLock lock(mutex_); for (const FutureHandle& h : clients_) { if (h != ReferenceCountedFutureImpl::kInvalidHandle) { api_->Complete(h, error, error_msg); @@ -120,6 +137,8 @@ class FutureProxyManager { ReferenceCountedFutureImpl* api_; // We need to keep the subject alive, as it owns us and the data. FutureHandle subject_; + // mutex to protect register/unregister operation. + mutable Mutex mutex_; }; struct CompletionCallbackData { @@ -245,7 +264,10 @@ FutureBackingData::~FutureBackingData() { context_data = nullptr; } - delete proxy; + if (proxy != nullptr) { + delete proxy; + proxy = nullptr; + } } void FutureBackingData::ClearExistingCallbacks() { @@ -466,11 +488,14 @@ void ReferenceCountedFutureImpl::ReleaseFuture(const FutureHandle& handle) { MutexLock lock(mutex_); FIREBASE_FUTURE_TRACE("API: Release future %d", (int)handle.id()); - // Assert if the handle isn't registered. // If a Future exists with a handle, then the backing should still exist for - // it, too. + // it, too. However it might be possible during the deallocate phase when + // FutureBase and FutureHandle and FutureProxyManager are still having + // dependencies. auto it = backings_.find(handle.id()); - FIREBASE_ASSERT(it != backings_.end()); + if (it == backings_.end()) { + return; + } // Decrement the reference count. FutureBackingData* backing = it->second; @@ -767,6 +792,17 @@ TypedCleanupNotifier& CleanupMgr( return static_cast(api)->cleanup_handles(); } +void ReferenceCountedFutureImpl::ForceReleaseFuture( + const FutureHandle& handle) { + MutexLock lock(mutex_); + FutureBackingData* backing = BackingFromHandle(handle.id()); + if (backing != nullptr) { + backing->reference_count = 1; + ReleaseFuture(handle); + } + FIREBASE_FUTURE_TRACE("API: ForceReleaseFuture handle %d", handle.id()); +} + // Implementation of FutureHandle from future.h FutureHandle::FutureHandle() : id_(0), api_(nullptr) {} diff --git a/app/src/reference_counted_future_impl.h b/app/src/reference_counted_future_impl.h index 88a48e77b8..f66772e7ce 100644 --- a/app/src/reference_counted_future_impl.h +++ b/app/src/reference_counted_future_impl.h @@ -419,6 +419,9 @@ class ReferenceCountedFutureImpl : public detail::FutureApiInterface { return cleanup_handles_; } + /// Force reset the ref count and release the handle. + void ForceReleaseFuture(const FutureHandle& handle); + private: template static void DeleteT(void* ptr_to_delete) { From a397c9662a3c05a40e0aa442324ec9b8d9c79c10 Mon Sep 17 00:00:00 2001 From: cynthiajiang Date: Thu, 18 Jun 2020 10:52:54 -0700 Subject: [PATCH 003/109] Fix integration test caused by curl http2 default setting PiperOrigin-RevId: 317136423 --- app/rest/transport_curl.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/rest/transport_curl.cc b/app/rest/transport_curl.cc index c904ca6caf..ac5bc7a8dd 100644 --- a/app/rest/transport_curl.cc +++ b/app/rest/transport_curl.cc @@ -474,6 +474,10 @@ bool BackgroundTransportCurl::PerformBackground(Request* request) { CheckOk(curl_easy_setopt(curl_, CURLOPT_TIMEOUT_MS, options.timeout_ms), "set http timeout milliseconds"); + // curl library is using http2 as default, so need to specify this. + CheckOk(curl_easy_setopt(curl_, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1), + "set http version to http1"); + // SDK error in initialization stage is not recoverable. FIREBASE_ASSERT(err_code_ == CURLE_OK); From f6e64438ec254bda2d23811c70b7917a8e77dc30 Mon Sep 17 00:00:00 2001 From: amaurice Date: Thu, 18 Jun 2020 11:14:43 -0700 Subject: [PATCH 004/109] Pin the Firestore pod used by the open source repo to the version from 6.26.0 PiperOrigin-RevId: 317141369 --- ios_pod/Podfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios_pod/Podfile b/ios_pod/Podfile index 0d6d7586dd..e41e99243c 100644 --- a/ios_pod/Podfile +++ b/ios_pod/Podfile @@ -9,7 +9,9 @@ target 'GetPods' do pod 'Firebase/Auth', '6.24.0' pod 'Firebase/Database', '6.24.0' pod 'Firebase/DynamicLinks', '6.24.0' - pod 'Firebase/Firestore', '6.24.0' + # Firestore is pinned to Firebase/Firebase 6.26.0 + # Return back to Firebase/Firebase when updating the rest + pod 'FirebaseFirestore', '1.15.0' pod 'Firebase/Functions', '6.24.0' pod 'FirebaseInstanceID', '4.3.4' pod 'Firebase/Messaging', '6.24.0' From fdd27b562ed23561d86e74bba89e427796a4b4ba Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 18 Jun 2020 11:14:43 -0700 Subject: [PATCH 005/109] Project import generated by Copybara. PiperOrigin-RevId: 317141369 --- admob/tools/ios/testapp/README.md | 57 + admob/tools/ios/testapp/p4_depot_paths | 7 + .../testapp/testapp.xcodeproj/project.pbxproj | 524 ++++ admob/tools/ios/testapp/testapp/AppDelegate.h | 11 + admob/tools/ios/testapp/testapp/AppDelegate.m | 16 + .../AppIcon.appiconset/Contents.json | 73 + .../testapp/Assets.xcassets/Contents.json | 6 + .../LaunchImages.launchimage/Contents.json | 36 + .../Base.lproj/LaunchScreen.storyboard | 27 + .../testapp/Base.lproj/Main.storyboard | 27 + admob/tools/ios/testapp/testapp/Info.plist | 50 + .../ios/testapp/testapp/ViewController.h | 8 + .../ios/testapp/testapp/ViewController.mm | 135 + .../tools/ios/testapp/testapp/game_engine.cpp | 551 ++++ admob/tools/ios/testapp/testapp/game_engine.h | 74 + admob/tools/ios/testapp/testapp/main.m | 11 + analytics/generate_constants_test.py | 176 ++ analytics/src_ios/fake/FIRAnalytics.h | 39 + analytics/src_ios/fake/FIRAnalytics.mm | 94 + .../firebase/analytics/FirebaseAnalytics.java | 91 + analytics/tests/CMakeLists.txt | 41 + analytics/tests/analytics_test.cc | 310 ++ .../instance_id_desktop_impl_test.cc | 819 +++++ app/memory/atomic_test.cc | 99 + app/memory/shared_ptr_test.cc | 229 ++ app/memory/unique_ptr_test.cc | 174 ++ app/meta/move_test.cc | 68 + app/rest/tests/gzipheader_unittest.cc | 162 + app/rest/tests/request_binary_test.cc | 96 + app/rest/tests/request_file_test.cc | 96 + app/rest/tests/request_json_test.cc | 71 + app/rest/tests/request_test.cc | 57 + app/rest/tests/request_test.h | 116 + app/rest/tests/response_binary_test.cc | 128 + app/rest/tests/response_json_test.cc | 130 + app/rest/tests/response_test.cc | 101 + app/rest/tests/testdata/sample.fbs | 24 + app/rest/tests/transport_curl_test.cc | 180 ++ app/rest/tests/transport_mock_test.cc | 75 + app/rest/tests/util_test.cc | 60 + app/rest/tests/www_form_url_encoded_test.cc | 107 + app/rest/tests/zlibwrapper_unittest.cc | 1050 +++++++ app/src/fake/FIRApp.h | 58 + app/src/fake/FIRApp.mm | 106 + app/src/fake/FIRConfiguration.h | 45 + app/src/fake/FIRConfiguration.m | 34 + app/src/fake/FIRLogger.h | 34 + app/src/fake/FIRLoggerLevel.h | 38 + app/src/fake/FIROptions.h | 51 + app/src/fake/FIROptions.mm | 66 + .../gms/common/GoogleApiAvailability.java | 48 + .../com/google/android/gms/tasks/Task.java | 114 + .../fake/com/google/firebase/FirebaseApp.java | 76 + .../google/firebase/FirebaseException.java | 25 + .../com/google/firebase/FirebaseOptions.java | 133 + .../app/internal/cpp/CppThreadDispatcher.java | 48 + .../cpp/GoogleApiAvailabilityHelper.java | 57 + .../app/internal/cpp/JniResultCallback.java | 103 + .../GlobalLibraryVersionRegistrar.java | 36 + app/tests/CMakeLists.txt | 410 +++ app/tests/app_test.cc | 599 ++++ app/tests/assert_test.cc | 283 ++ app/tests/base64_openssh_test.cc | 98 + app/tests/base64_test.cc | 221 ++ app/tests/callback_test.cc | 462 +++ app/tests/cleanup_notifier_test.cc | 417 +++ app/tests/flexbuffer_matcher.cc | 252 ++ app/tests/flexbuffer_matcher.h | 56 + app/tests/flexbuffer_matcher_test.cc | 236 ++ app/tests/future_manager_test.cc | 205 ++ app/tests/future_test.cc | 1567 ++++++++++ .../availability_android_test.cc | 242 ++ app/tests/google_services_test.cc | 75 + app/tests/include/firebase/app_for_testing.h | 59 + app/tests/intrusive_list_test.cc | 1229 ++++++++ app/tests/jobject_reference_test.cc | 162 + app/tests/locale_test.cc | 56 + app/tests/log_test.cc | 55 + app/tests/logger_test.cc | 283 ++ app/tests/optional_test.cc | 446 +++ app/tests/path_test.cc | 452 +++ app/tests/reference_count_test.cc | 275 ++ app/tests/scheduler_test.cc | 369 +++ .../secure/user_secure_integration_test.cc | 255 ++ app/tests/secure/user_secure_internal_test.cc | 285 ++ app/tests/secure/user_secure_manager_test.cc | 178 ++ app/tests/semaphore_test.cc | 96 + app/tests/swizzle_test.mm | 131 + app/tests/thread_test.cc | 194 ++ app/tests/time_test.cc | 101 + app/tests/util_android_test.cc | 479 +++ app/tests/util_ios_test.mm | 650 ++++ app/tests/uuid_test.cc | 42 + app/tests/variant_test.cc | 1186 +++++++ app/tests/variant_util_test.cc | 549 ++++ auth/src/ios/fake/FIRActionCodeSettings.h | 89 + auth/src/ios/fake/FIRAdditionalUserInfo.h | 61 + auth/src/ios/fake/FIRAdditionalUserInfo.mm | 35 + auth/src/ios/fake/FIRAuth.h | 832 +++++ auth/src/ios/fake/FIRAuth.mm | 214 ++ auth/src/ios/fake/FIRAuthAPNSTokenType.h | 40 + auth/src/ios/fake/FIRAuthCredential.h | 44 + auth/src/ios/fake/FIRAuthCredential.mm | 33 + auth/src/ios/fake/FIRAuthDataResult.h | 61 + auth/src/ios/fake/FIRAuthDataResult.mm | 37 + auth/src/ios/fake/FIRAuthErrors.h | 358 +++ auth/src/ios/fake/FIRAuthSettings.h | 35 + auth/src/ios/fake/FIRAuthTokenResult.h | 66 + auth/src/ios/fake/FIRAuthUIDelegate.h | 53 + auth/src/ios/fake/FIREmailAuthProvider.h | 70 + auth/src/ios/fake/FIREmailAuthProvider.mm | 35 + auth/src/ios/fake/FIRFacebookAuthProvider.h | 54 + auth/src/ios/fake/FIRFacebookAuthProvider.mm | 30 + auth/src/ios/fake/FIRFederatedAuthProvider.h | 52 + auth/src/ios/fake/FIRGameCenterAuthProvider.h | 62 + .../src/ios/fake/FIRGameCenterAuthProvider.mm | 27 + auth/src/ios/fake/FIRGitHubAuthProvider.h | 55 + auth/src/ios/fake/FIRGitHubAuthProvider.mm | 31 + auth/src/ios/fake/FIRGoogleAuthProvider.h | 56 + auth/src/ios/fake/FIRGoogleAuthProvider.mm | 32 + auth/src/ios/fake/FIROAuthCredential.h | 55 + auth/src/ios/fake/FIROAuthCredential.mm | 35 + auth/src/ios/fake/FIROAuthProvider.h | 113 + auth/src/ios/fake/FIROAuthProvider.mm | 68 + auth/src/ios/fake/FIRPhoneAuthCredential.h | 38 + auth/src/ios/fake/FIRPhoneAuthCredential.mm | 35 + auth/src/ios/fake/FIRPhoneAuthProvider.h | 109 + auth/src/ios/fake/FIRPhoneAuthProvider.mm | 49 + auth/src/ios/fake/FIRTwitterAuthProvider.h | 54 + auth/src/ios/fake/FIRTwitterAuthProvider.mm | 31 + auth/src/ios/fake/FIRUser.h | 507 +++ auth/src/ios/fake/FIRUser.mm | 161 + auth/src/ios/fake/FIRUserInfo.h | 60 + auth/src/ios/fake/FIRUserMetadata.h | 49 + auth/src/ios/fake/FIRUserMetadata.mm | 34 + auth/src/ios/fake/FirebaseAuth.h | 46 + auth/src/ios/fake/FirebaseAuthVersion.h | 27 + .../FirebaseApiNotAvailableException.java | 25 + .../firebase/FirebaseNetworkException.java | 25 + .../FirebaseTooManyRequestsException.java | 25 + .../firebase/auth/AdditionalUserInfo.java | 35 + .../google/firebase/auth/AuthCredential.java | 32 + .../com/google/firebase/auth/AuthResult.java | 29 + .../firebase/auth/EmailAuthProvider.java | 25 + .../firebase/auth/FacebookAuthProvider.java | 25 + .../firebase/auth/FederatedAuthProvider.java | 22 + .../google/firebase/auth/FirebaseAuth.java | 242 ++ .../auth/FirebaseAuthActionCodeException.java | 25 + .../auth/FirebaseAuthEmailException.java | 25 + .../firebase/auth/FirebaseAuthException.java | 34 + ...rebaseAuthInvalidCredentialsException.java | 25 + .../FirebaseAuthInvalidUserException.java | 25 + ...ebaseAuthRecentLoginRequiredException.java | 25 + .../FirebaseAuthUserCollisionException.java | 25 + .../FirebaseAuthWeakPasswordException.java | 29 + .../auth/FirebaseAuthWebException.java | 25 + .../google/firebase/auth/FirebaseUser.java | 201 ++ .../firebase/auth/FirebaseUserMetadata.java | 31 + .../google/firebase/auth/GetTokenResult.java | 25 + .../firebase/auth/GithubAuthProvider.java | 25 + .../firebase/auth/GoogleAuthProvider.java | 25 + .../google/firebase/auth/OAuthProvider.java | 138 + .../firebase/auth/PhoneAuthCredential.java | 24 + .../firebase/auth/PhoneAuthProvider.java | 56 + .../firebase/auth/PlayGamesAuthProvider.java | 25 + .../auth/SignInMethodQueryResult.java | 27 + .../firebase/auth/TwitterAuthProvider.java | 25 + .../com/google/firebase/auth/UserInfo.java | 53 + .../auth/UserProfileChangeRequest.java | 38 + auth/tests/CMakeLists.txt | 282 ++ auth/tests/auth_test.cc | 558 ++++ auth/tests/credential_test.cc | 113 + auth/tests/desktop/auth_desktop_test.cc | 895 ++++++ auth/tests/desktop/fakes.cc | 118 + auth/tests/desktop/fakes.h | 64 + .../desktop/rpcs/create_auth_uri_test.cc | 66 + .../tests/desktop/rpcs/delete_account_test.cc | 57 + .../desktop/rpcs/get_account_info_test.cc | 81 + .../rpcs/get_oob_confirmation_code_test.cc | 81 + .../tests/desktop/rpcs/reset_password_test.cc | 61 + auth/tests/desktop/rpcs/secure_token_test.cc | 68 + .../desktop/rpcs/set_account_info_test.cc | 173 + .../desktop/rpcs/sign_up_new_user_test.cc | 110 + auth/tests/desktop/rpcs/test_util.cc | 69 + auth/tests/desktop/rpcs/test_util.h | 39 + .../desktop/rpcs/verify_assertion_test.cc | 87 + .../desktop/rpcs/verify_custom_token_test.cc | 90 + .../desktop/rpcs/verify_password_test.cc | 105 + auth/tests/desktop/test_utils.cc | 71 + auth/tests/desktop/test_utils.h | 295 ++ auth/tests/desktop/user_desktop_test.cc | 1217 ++++++++ auth/tests/user_test.cc | 520 +++ binary_to_array_test.py | 91 + build_type_header_test.py | 46 + database/src/ios/util_ios_test.mm | 111 + database/tests/CMakeLists.txt | 343 ++ .../tests/common/database_reference_test.cc | 283 ++ .../desktop/connection/connection_test.cc | 301 ++ .../connection/web_socket_client_impl_test.cc | 268 ++ .../tests/desktop/core/cache_policy_test.cc | 70 + .../tests/desktop/core/compound_write_test.cc | 545 ++++ .../desktop/core/event_registration_test.cc | 186 ++ .../desktop/core/indexed_variant_test.cc | 677 ++++ database/tests/desktop/core/operation_test.cc | 424 +++ .../tests/desktop/core/server_values_test.cc | 221 ++ .../desktop/core/sparse_snapshot_tree_test.cc | 116 + .../tests/desktop/core/sync_point_test.cc | 390 +++ database/tests/desktop/core/sync_tree_test.cc | 825 +++++ .../core/tracked_query_manager_test.cc | 396 +++ database/tests/desktop/core/tree_test.cc | 1009 ++++++ .../tests/desktop/core/write_tree_test.cc | 792 +++++ .../desktop/mutable_data_desktop_test.cc | 237 ++ .../flatbuffer_conversions_test.cc | 458 +++ ..._memory_persistence_storage_engine_test.cc | 415 +++ ...evel_db_persistence_storage_engine_test.cc | 700 +++++ .../noop_persistence_manager_test.cc | 86 + .../persistence/persistence_manager_test.cc | 461 +++ .../desktop/persistence/prune_forest_test.cc | 485 +++ .../desktop/push_child_name_generator_test.cc | 87 + database/tests/desktop/test/matchers.h | 40 + database/tests/desktop/test/matchers_test.cc | 73 + .../tests/desktop/test/mock_cache_policy.h | 45 + .../tests/desktop/test/mock_listen_provider.h | 40 + database/tests/desktop/test/mock_listener.h | 55 + .../desktop/test/mock_persistence_manager.h | 77 + .../test/mock_persistence_storage_engine.h | 79 + .../desktop/test/mock_tracked_query_manager.h | 52 + database/tests/desktop/test/mock_write_tree.h | 80 + database/tests/desktop/util_desktop_test.cc | 2775 +++++++++++++++++ database/tests/desktop/view/change_test.cc | 341 ++ .../view/child_change_accumulator_test.cc | 182 ++ .../desktop/view/event_generator_test.cc | 346 ++ .../tests/desktop/view/indexed_filter_test.cc | 391 +++ .../tests/desktop/view/limited_filter_test.cc | 314 ++ .../tests/desktop/view/ranged_filter_test.cc | 673 ++++ .../tests/desktop/view/view_cache_test.cc | 133 + .../tests/desktop/view/view_processor_test.cc | 727 +++++ database/tests/desktop/view/view_test.cc | 464 +++ firestore/generate_android_test.py | 88 + .../tests/android/field_path_portable_test.cc | 140 + ...irebase_firestore_settings_android_test.cc | 52 + .../tests/android/geo_point_android_test.cc | 24 + .../android/snapshot_metadata_android_test.cc | 42 + .../tests/android/timestamp_android_test.cc | 26 + firestore/src/tests/array_transform_test.cc | 230 ++ firestore/src/tests/cleanup_test.cc | 420 +++ .../src/tests/collection_reference_test.cc | 34 + firestore/src/tests/cursor_test.cc | 278 ++ firestore/src/tests/document_change_test.cc | 31 + .../src/tests/document_reference_test.cc | 34 + firestore/src/tests/document_snapshot_test.cc | 35 + firestore/src/tests/field_value_test.cc | 331 ++ firestore/src/tests/fields_test.cc | 229 ++ .../src/tests/firestore_integration_test.cc | 219 ++ .../src/tests/firestore_integration_test.h | 275 ++ firestore/src/tests/firestore_test.cc | 1334 ++++++++ firestore/src/tests/includes_test.cc | 87 + .../src/tests/listener_registration_test.cc | 185 ++ .../src/tests/numeric_transforms_test.cc | 204 ++ firestore/src/tests/query_network_test.cc | 148 + firestore/src/tests/query_snapshot_test.cc | 34 + firestore/src/tests/query_test.cc | 697 +++++ firestore/src/tests/sanity_test.cc | 38 + firestore/src/tests/server_timestamp_test.cc | 294 ++ firestore/src/tests/smoke_test.cc | 165 + firestore/src/tests/transaction_extra_test.cc | 114 + firestore/src/tests/transaction_test.cc | 750 +++++ firestore/src/tests/type_test.cc | 71 + .../src/tests/util/integration_test_util.cc | 66 + .../tests/util/integration_test_util_apple.mm | 21 + firestore/src/tests/validation_test.cc | 885 ++++++ firestore/src/tests/write_batch_test.cc | 314 ++ instance_id/src_ios/fake/FIRInstanceID.h | 330 ++ instance_id/src_ios/fake/FIRInstanceID.mm | 158 + .../firebase/iid/FirebaseInstanceId.java | 187 ++ instance_id/tests/CMakeLists.txt | 36 + instance_id/tests/instance_id_test.cc | 546 ++++ .../MessageForwardingServiceTest.java | 82 + .../firebase/messaging/RemoteMessageUtil.java | 31 + .../messaging/cpp/ListenerServiceTest.java | 86 + .../messaging/cpp/MessageWriterTest.java | 91 + messaging/src/ios/fake/FIRMessaging.h | 507 +++ messaging/src/ios/fake/FIRMessaging.mm | 125 + .../firebase/messaging/FirebaseMessaging.java | 81 + .../firebase/messaging/RemoteMessage.java | 73 + .../cpp/RegistrationIntentService.java | 21 + messaging/tests/CMakeLists.txt | 78 + .../tests/android/cpp/message_reader_test.cc | 289 ++ .../tests/android/cpp/messaging_test_util.cc | 277 ++ messaging/tests/ios/messaging_test_util.mm | 99 + messaging/tests/messaging_test.cc | 380 +++ messaging/tests/messaging_test_util.h | 51 + remote_config/src/desktop/rest_fake.cc | 73 + .../remoteconfig/FirebaseRemoteConfig.java | 269 ++ ...seRemoteConfigFetchThrottledException.java | 24 + .../FirebaseRemoteConfigInfo.java | 44 + .../FirebaseRemoteConfigSettings.java | 45 + .../FirebaseRemoteConfigValue.java | 72 + remote_config/tests/CMakeLists.txt | 37 + .../tests/desktop/config_data_test.cc | 156 + .../tests/desktop/file_manager_test.cc | 66 + remote_config/tests/desktop/metadata_test.cc | 101 + .../desktop/notification_channel_test.cc | 79 + .../desktop/remote_config_desktop_test.cc | 528 ++++ remote_config/tests/desktop/rest_test.cc | 502 +++ remote_config/tests/remote_config_test.cc | 692 ++++ storage/src/common/storage_uri_parser_test.cc | 153 + storage/tests/CMakeLists.txt | 25 + .../desktop/storage_desktop_utils_tests.cc | 195 ++ testing/config_test.cc | 174 ++ testing/reporter_impl_fake.cc | 34 + testing/reporter_impl_test.cc | 45 + testing/reporter_test.cc | 190 ++ testing/ticker_test.cc | 170 + testing/util_android_test.cc | 86 + testing/util_ios_test.mm | 80 + version_header_test.py | 103 + 317 files changed, 62778 insertions(+) create mode 100644 admob/tools/ios/testapp/README.md create mode 100644 admob/tools/ios/testapp/p4_depot_paths create mode 100644 admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj create mode 100644 admob/tools/ios/testapp/testapp/AppDelegate.h create mode 100644 admob/tools/ios/testapp/testapp/AppDelegate.m create mode 100644 admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json create mode 100644 admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json create mode 100644 admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard create mode 100644 admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard create mode 100644 admob/tools/ios/testapp/testapp/Info.plist create mode 100644 admob/tools/ios/testapp/testapp/ViewController.h create mode 100644 admob/tools/ios/testapp/testapp/ViewController.mm create mode 100644 admob/tools/ios/testapp/testapp/game_engine.cpp create mode 100644 admob/tools/ios/testapp/testapp/game_engine.h create mode 100644 admob/tools/ios/testapp/testapp/main.m create mode 100644 analytics/generate_constants_test.py create mode 100644 analytics/src_ios/fake/FIRAnalytics.h create mode 100644 analytics/src_ios/fake/FIRAnalytics.mm create mode 100644 analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java create mode 100644 analytics/tests/CMakeLists.txt create mode 100644 analytics/tests/analytics_test.cc create mode 100644 app/instance_id/instance_id_desktop_impl_test.cc create mode 100644 app/memory/atomic_test.cc create mode 100644 app/memory/shared_ptr_test.cc create mode 100644 app/memory/unique_ptr_test.cc create mode 100644 app/meta/move_test.cc create mode 100644 app/rest/tests/gzipheader_unittest.cc create mode 100644 app/rest/tests/request_binary_test.cc create mode 100644 app/rest/tests/request_file_test.cc create mode 100644 app/rest/tests/request_json_test.cc create mode 100644 app/rest/tests/request_test.cc create mode 100644 app/rest/tests/request_test.h create mode 100644 app/rest/tests/response_binary_test.cc create mode 100644 app/rest/tests/response_json_test.cc create mode 100644 app/rest/tests/response_test.cc create mode 100644 app/rest/tests/testdata/sample.fbs create mode 100644 app/rest/tests/transport_curl_test.cc create mode 100644 app/rest/tests/transport_mock_test.cc create mode 100644 app/rest/tests/util_test.cc create mode 100644 app/rest/tests/www_form_url_encoded_test.cc create mode 100644 app/rest/tests/zlibwrapper_unittest.cc create mode 100644 app/src/fake/FIRApp.h create mode 100644 app/src/fake/FIRApp.mm create mode 100644 app/src/fake/FIRConfiguration.h create mode 100644 app/src/fake/FIRConfiguration.m create mode 100644 app/src/fake/FIRLogger.h create mode 100644 app/src/fake/FIRLoggerLevel.h create mode 100644 app/src/fake/FIROptions.h create mode 100644 app/src/fake/FIROptions.mm create mode 100644 app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java create mode 100644 app/src_java/fake/com/google/android/gms/tasks/Task.java create mode 100644 app/src_java/fake/com/google/firebase/FirebaseApp.java create mode 100644 app/src_java/fake/com/google/firebase/FirebaseException.java create mode 100644 app/src_java/fake/com/google/firebase/FirebaseOptions.java create mode 100644 app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java create mode 100644 app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java create mode 100644 app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java create mode 100644 app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java create mode 100644 app/tests/CMakeLists.txt create mode 100644 app/tests/app_test.cc create mode 100644 app/tests/assert_test.cc create mode 100644 app/tests/base64_openssh_test.cc create mode 100644 app/tests/base64_test.cc create mode 100644 app/tests/callback_test.cc create mode 100644 app/tests/cleanup_notifier_test.cc create mode 100644 app/tests/flexbuffer_matcher.cc create mode 100644 app/tests/flexbuffer_matcher.h create mode 100644 app/tests/flexbuffer_matcher_test.cc create mode 100644 app/tests/future_manager_test.cc create mode 100644 app/tests/future_test.cc create mode 100644 app/tests/google_play_services/availability_android_test.cc create mode 100644 app/tests/google_services_test.cc create mode 100644 app/tests/include/firebase/app_for_testing.h create mode 100644 app/tests/intrusive_list_test.cc create mode 100644 app/tests/jobject_reference_test.cc create mode 100644 app/tests/locale_test.cc create mode 100644 app/tests/log_test.cc create mode 100644 app/tests/logger_test.cc create mode 100644 app/tests/optional_test.cc create mode 100644 app/tests/path_test.cc create mode 100644 app/tests/reference_count_test.cc create mode 100644 app/tests/scheduler_test.cc create mode 100644 app/tests/secure/user_secure_integration_test.cc create mode 100644 app/tests/secure/user_secure_internal_test.cc create mode 100644 app/tests/secure/user_secure_manager_test.cc create mode 100644 app/tests/semaphore_test.cc create mode 100644 app/tests/swizzle_test.mm create mode 100644 app/tests/thread_test.cc create mode 100644 app/tests/time_test.cc create mode 100644 app/tests/util_android_test.cc create mode 100644 app/tests/util_ios_test.mm create mode 100644 app/tests/uuid_test.cc create mode 100644 app/tests/variant_test.cc create mode 100644 app/tests/variant_util_test.cc create mode 100644 auth/src/ios/fake/FIRActionCodeSettings.h create mode 100644 auth/src/ios/fake/FIRAdditionalUserInfo.h create mode 100644 auth/src/ios/fake/FIRAdditionalUserInfo.mm create mode 100644 auth/src/ios/fake/FIRAuth.h create mode 100644 auth/src/ios/fake/FIRAuth.mm create mode 100644 auth/src/ios/fake/FIRAuthAPNSTokenType.h create mode 100644 auth/src/ios/fake/FIRAuthCredential.h create mode 100644 auth/src/ios/fake/FIRAuthCredential.mm create mode 100644 auth/src/ios/fake/FIRAuthDataResult.h create mode 100644 auth/src/ios/fake/FIRAuthDataResult.mm create mode 100644 auth/src/ios/fake/FIRAuthErrors.h create mode 100644 auth/src/ios/fake/FIRAuthSettings.h create mode 100644 auth/src/ios/fake/FIRAuthTokenResult.h create mode 100644 auth/src/ios/fake/FIRAuthUIDelegate.h create mode 100644 auth/src/ios/fake/FIREmailAuthProvider.h create mode 100644 auth/src/ios/fake/FIREmailAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRFacebookAuthProvider.h create mode 100644 auth/src/ios/fake/FIRFacebookAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRFederatedAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGameCenterAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGameCenterAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRGitHubAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGitHubAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRGoogleAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGoogleAuthProvider.mm create mode 100644 auth/src/ios/fake/FIROAuthCredential.h create mode 100644 auth/src/ios/fake/FIROAuthCredential.mm create mode 100644 auth/src/ios/fake/FIROAuthProvider.h create mode 100644 auth/src/ios/fake/FIROAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRPhoneAuthCredential.h create mode 100644 auth/src/ios/fake/FIRPhoneAuthCredential.mm create mode 100644 auth/src/ios/fake/FIRPhoneAuthProvider.h create mode 100644 auth/src/ios/fake/FIRPhoneAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRTwitterAuthProvider.h create mode 100644 auth/src/ios/fake/FIRTwitterAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRUser.h create mode 100644 auth/src/ios/fake/FIRUser.mm create mode 100644 auth/src/ios/fake/FIRUserInfo.h create mode 100644 auth/src/ios/fake/FIRUserMetadata.h create mode 100644 auth/src/ios/fake/FIRUserMetadata.mm create mode 100644 auth/src/ios/fake/FirebaseAuth.h create mode 100644 auth/src/ios/fake/FirebaseAuthVersion.h create mode 100644 auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java create mode 100644 auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java create mode 100644 auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/AuthCredential.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/AuthResult.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/UserInfo.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java create mode 100644 auth/tests/CMakeLists.txt create mode 100644 auth/tests/auth_test.cc create mode 100644 auth/tests/credential_test.cc create mode 100644 auth/tests/desktop/auth_desktop_test.cc create mode 100644 auth/tests/desktop/fakes.cc create mode 100644 auth/tests/desktop/fakes.h create mode 100644 auth/tests/desktop/rpcs/create_auth_uri_test.cc create mode 100644 auth/tests/desktop/rpcs/delete_account_test.cc create mode 100644 auth/tests/desktop/rpcs/get_account_info_test.cc create mode 100644 auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc create mode 100644 auth/tests/desktop/rpcs/reset_password_test.cc create mode 100644 auth/tests/desktop/rpcs/secure_token_test.cc create mode 100644 auth/tests/desktop/rpcs/set_account_info_test.cc create mode 100644 auth/tests/desktop/rpcs/sign_up_new_user_test.cc create mode 100644 auth/tests/desktop/rpcs/test_util.cc create mode 100644 auth/tests/desktop/rpcs/test_util.h create mode 100644 auth/tests/desktop/rpcs/verify_assertion_test.cc create mode 100644 auth/tests/desktop/rpcs/verify_custom_token_test.cc create mode 100644 auth/tests/desktop/rpcs/verify_password_test.cc create mode 100644 auth/tests/desktop/test_utils.cc create mode 100644 auth/tests/desktop/test_utils.h create mode 100644 auth/tests/desktop/user_desktop_test.cc create mode 100644 auth/tests/user_test.cc create mode 100644 binary_to_array_test.py create mode 100644 build_type_header_test.py create mode 100644 database/src/ios/util_ios_test.mm create mode 100644 database/tests/CMakeLists.txt create mode 100644 database/tests/common/database_reference_test.cc create mode 100644 database/tests/desktop/connection/connection_test.cc create mode 100644 database/tests/desktop/connection/web_socket_client_impl_test.cc create mode 100644 database/tests/desktop/core/cache_policy_test.cc create mode 100644 database/tests/desktop/core/compound_write_test.cc create mode 100644 database/tests/desktop/core/event_registration_test.cc create mode 100644 database/tests/desktop/core/indexed_variant_test.cc create mode 100644 database/tests/desktop/core/operation_test.cc create mode 100644 database/tests/desktop/core/server_values_test.cc create mode 100644 database/tests/desktop/core/sparse_snapshot_tree_test.cc create mode 100644 database/tests/desktop/core/sync_point_test.cc create mode 100644 database/tests/desktop/core/sync_tree_test.cc create mode 100644 database/tests/desktop/core/tracked_query_manager_test.cc create mode 100644 database/tests/desktop/core/tree_test.cc create mode 100644 database/tests/desktop/core/write_tree_test.cc create mode 100644 database/tests/desktop/mutable_data_desktop_test.cc create mode 100644 database/tests/desktop/persistence/flatbuffer_conversions_test.cc create mode 100644 database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc create mode 100644 database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc create mode 100644 database/tests/desktop/persistence/noop_persistence_manager_test.cc create mode 100644 database/tests/desktop/persistence/persistence_manager_test.cc create mode 100644 database/tests/desktop/persistence/prune_forest_test.cc create mode 100644 database/tests/desktop/push_child_name_generator_test.cc create mode 100644 database/tests/desktop/test/matchers.h create mode 100644 database/tests/desktop/test/matchers_test.cc create mode 100644 database/tests/desktop/test/mock_cache_policy.h create mode 100644 database/tests/desktop/test/mock_listen_provider.h create mode 100644 database/tests/desktop/test/mock_listener.h create mode 100644 database/tests/desktop/test/mock_persistence_manager.h create mode 100644 database/tests/desktop/test/mock_persistence_storage_engine.h create mode 100644 database/tests/desktop/test/mock_tracked_query_manager.h create mode 100644 database/tests/desktop/test/mock_write_tree.h create mode 100644 database/tests/desktop/util_desktop_test.cc create mode 100644 database/tests/desktop/view/change_test.cc create mode 100644 database/tests/desktop/view/child_change_accumulator_test.cc create mode 100644 database/tests/desktop/view/event_generator_test.cc create mode 100644 database/tests/desktop/view/indexed_filter_test.cc create mode 100644 database/tests/desktop/view/limited_filter_test.cc create mode 100644 database/tests/desktop/view/ranged_filter_test.cc create mode 100644 database/tests/desktop/view/view_cache_test.cc create mode 100644 database/tests/desktop/view/view_processor_test.cc create mode 100644 database/tests/desktop/view/view_test.cc create mode 100755 firestore/generate_android_test.py create mode 100644 firestore/src/tests/android/field_path_portable_test.cc create mode 100644 firestore/src/tests/android/firebase_firestore_settings_android_test.cc create mode 100644 firestore/src/tests/android/geo_point_android_test.cc create mode 100644 firestore/src/tests/android/snapshot_metadata_android_test.cc create mode 100644 firestore/src/tests/android/timestamp_android_test.cc create mode 100644 firestore/src/tests/array_transform_test.cc create mode 100644 firestore/src/tests/cleanup_test.cc create mode 100644 firestore/src/tests/collection_reference_test.cc create mode 100644 firestore/src/tests/cursor_test.cc create mode 100644 firestore/src/tests/document_change_test.cc create mode 100644 firestore/src/tests/document_reference_test.cc create mode 100644 firestore/src/tests/document_snapshot_test.cc create mode 100644 firestore/src/tests/field_value_test.cc create mode 100644 firestore/src/tests/fields_test.cc create mode 100644 firestore/src/tests/firestore_integration_test.cc create mode 100644 firestore/src/tests/firestore_integration_test.h create mode 100644 firestore/src/tests/firestore_test.cc create mode 100644 firestore/src/tests/includes_test.cc create mode 100644 firestore/src/tests/listener_registration_test.cc create mode 100644 firestore/src/tests/numeric_transforms_test.cc create mode 100644 firestore/src/tests/query_network_test.cc create mode 100644 firestore/src/tests/query_snapshot_test.cc create mode 100644 firestore/src/tests/query_test.cc create mode 100644 firestore/src/tests/sanity_test.cc create mode 100644 firestore/src/tests/server_timestamp_test.cc create mode 100644 firestore/src/tests/smoke_test.cc create mode 100644 firestore/src/tests/transaction_extra_test.cc create mode 100644 firestore/src/tests/transaction_test.cc create mode 100644 firestore/src/tests/type_test.cc create mode 100644 firestore/src/tests/util/integration_test_util.cc create mode 100644 firestore/src/tests/util/integration_test_util_apple.mm create mode 100644 firestore/src/tests/validation_test.cc create mode 100644 firestore/src/tests/write_batch_test.cc create mode 100644 instance_id/src_ios/fake/FIRInstanceID.h create mode 100644 instance_id/src_ios/fake/FIRInstanceID.mm create mode 100644 instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java create mode 100644 instance_id/tests/CMakeLists.txt create mode 100644 instance_id/tests/instance_id_test.cc create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java create mode 100644 messaging/src/ios/fake/FIRMessaging.h create mode 100644 messaging/src/ios/fake/FIRMessaging.mm create mode 100644 messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java create mode 100644 messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java create mode 100644 messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java create mode 100644 messaging/tests/CMakeLists.txt create mode 100644 messaging/tests/android/cpp/message_reader_test.cc create mode 100644 messaging/tests/android/cpp/messaging_test_util.cc create mode 100644 messaging/tests/ios/messaging_test_util.mm create mode 100644 messaging/tests/messaging_test.cc create mode 100644 messaging/tests/messaging_test_util.h create mode 100644 remote_config/src/desktop/rest_fake.cc create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java create mode 100644 remote_config/tests/CMakeLists.txt create mode 100644 remote_config/tests/desktop/config_data_test.cc create mode 100644 remote_config/tests/desktop/file_manager_test.cc create mode 100644 remote_config/tests/desktop/metadata_test.cc create mode 100644 remote_config/tests/desktop/notification_channel_test.cc create mode 100644 remote_config/tests/desktop/remote_config_desktop_test.cc create mode 100644 remote_config/tests/desktop/rest_test.cc create mode 100644 remote_config/tests/remote_config_test.cc create mode 100644 storage/src/common/storage_uri_parser_test.cc create mode 100644 storage/tests/CMakeLists.txt create mode 100644 storage/tests/desktop/storage_desktop_utils_tests.cc create mode 100644 testing/config_test.cc create mode 100644 testing/reporter_impl_fake.cc create mode 100644 testing/reporter_impl_test.cc create mode 100644 testing/reporter_test.cc create mode 100644 testing/ticker_test.cc create mode 100644 testing/util_android_test.cc create mode 100644 testing/util_ios_test.mm create mode 100644 version_header_test.py diff --git a/admob/tools/ios/testapp/README.md b/admob/tools/ios/testapp/README.md new file mode 100644 index 0000000000..ad1c85a36d --- /dev/null +++ b/admob/tools/ios/testapp/README.md @@ -0,0 +1,57 @@ +Firebase AdMob iOS Test App +=========================== + +The Firebase AdMob iOS test app is designed to enable implementing, modifying, +and debugging API features directly in Xcode. + +Getting Started +--------------- + +- Get the code: + + git5 init + git5-track-p4-depot-paths //depot_firebase_cpp/admob/client/cpp/tools/ios/testapp/p4_depot_paths + git5 sync + +- Create the following symlinks (DO NOT check these in to google3 -- they should be added to your + .gitignore): + + NOTE: Firebase changed their includes from `include` to `src/include`. + These soft links work around the issue. + + GOOGLE3_PATH=~/path/to/git5/repo/google3 # Change to your google3 path + ln -s $GOOGLE3_PATH/firebase/app/client/cpp/src/include/ $GOOGLE3_PATH/firebase/app/client/cpp/include + ln -s $GOOGLE3_PATH/firebase/admob/client/cpp/src/include/ $GOOGLE3_PATH/firebase/admob/client/cpp/include + +Setting up the App +------------------ + +- In Project Navigator, add the GoogleMobileAds.framework to the Frameworks + testapp project. +- Update the following files: + - google3/firebase/admob/client/cpp/src/common/admob_common.cc + - Comment out the following code: + + /* + FIREBASE_APP_REGISTER_CALLBACKS(admob, + { + if (app == ::firebase::App::GetInstance()) { + return firebase::admob::Initialize(*app); + } + return kInitResultSuccess; + }, + { + if (app == ::firebase::App::GetInstance()) { + firebase::admob::Terminate(); + } + }); + */ + + - google3/firebase/admob/client/cpp/src/include/firebase/admob.h + - Comment out the following code: + + /* + #if !defined(DOXYGEN) && !defined(SWIG) + FIREBASE_APP_REGISTER_CALLBACKS_REFERENCE(admob) + #endif // !defined(DOXYGEN) && !defined(SWIG) + */ diff --git a/admob/tools/ios/testapp/p4_depot_paths b/admob/tools/ios/testapp/p4_depot_paths new file mode 100644 index 0000000000..96e3e23316 --- /dev/null +++ b/admob/tools/ios/testapp/p4_depot_paths @@ -0,0 +1,7 @@ +# Run the following command to git5 track the required directories for the +# Firebase-AdMob iOS test app: +# +# $ git5-track-p4-depot-paths //depot_firebase_cpp/admob/client/cpp/tools/ios/testapp/p4_depot_paths + +//depot_firebase_cpp/app/... +//depot_firebase_cpp/admob/... diff --git a/admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj b/admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a5be7f010d --- /dev/null +++ b/admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj @@ -0,0 +1,524 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 4AA541CC1CC6A9B400973957 /* GLKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AA541CB1CC6A9B400973957 /* GLKit.framework */; }; + 4AD13EA51CC9763C00AB0ACF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13E981CC9763C00AB0ACF /* AppDelegate.m */; }; + 4AD13EA61CC9763C00AB0ACF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AD13E991CC9763C00AB0ACF /* Assets.xcassets */; }; + 4AD13EA91CC9763C00AB0ACF /* game_engine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13E9F1CC9763C00AB0ACF /* game_engine.cpp */; }; + 4AD13EAB1CC9763C00AB0ACF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13EA21CC9763C00AB0ACF /* main.m */; }; + 4AD13EAC1CC9763C00AB0ACF /* ViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13EA41CC9763C00AB0ACF /* ViewController.mm */; }; + 4AD13EB11CC976C200AB0ACF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AD13EAD1CC976C200AB0ACF /* LaunchScreen.storyboard */; }; + 4AD13EB21CC976C200AB0ACF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AD13EAF1CC976C200AB0ACF /* Main.storyboard */; }; + 4AE90DEE1DBEC0AA00865A75 /* log_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DED1DBEC0AA00865A75 /* log_ios.mm */; }; + 4AE90DF61DBEC0DC00865A75 /* log.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DEF1DBEC0DC00865A75 /* log.cc */; }; + 4AE90DF71DBEC0DC00865A75 /* reference_counted_future_impl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DF21DBEC0DC00865A75 /* reference_counted_future_impl.cc */; }; + 4AE90DF81DBEC0DC00865A75 /* util_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DF51DBEC0DC00865A75 /* util_ios.mm */; }; + 4AE90E0C1DBEC0F300865A75 /* admob_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DF91DBEC0F300865A75 /* admob_ios.mm */; }; + 4AE90E0D1DBEC0F300865A75 /* banner_view_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DFB1DBEC0F300865A75 /* banner_view_internal_ios.mm */; }; + 4AE90E0E1DBEC0F300865A75 /* FADBannerView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DFD1DBEC0F300865A75 /* FADBannerView.mm */; }; + 4AE90E0F1DBEC0F300865A75 /* FADInterstitialDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DFF1DBEC0F300865A75 /* FADInterstitialDelegate.mm */; }; + 4AE90E101DBEC0F300865A75 /* FADNativeExpressAdView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E011DBEC0F300865A75 /* FADNativeExpressAdView.mm */; }; + 4AE90E111DBEC0F300865A75 /* FADRequest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E031DBEC0F300865A75 /* FADRequest.mm */; }; + 4AE90E121DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E051DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm */; }; + 4AE90E131DBEC0F300865A75 /* interstitial_ad_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E071DBEC0F300865A75 /* interstitial_ad_internal_ios.mm */; }; + 4AE90E141DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E091DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm */; }; + 4AE90E151DBEC0F300865A75 /* rewarded_video_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E0B1DBEC0F300865A75 /* rewarded_video_internal_ios.mm */; }; + 4AE90E241DBEC10700865A75 /* admob_common.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E161DBEC10700865A75 /* admob_common.cc */; }; + 4AE90E251DBEC10700865A75 /* banner_view_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E181DBEC10700865A75 /* banner_view_internal.cc */; }; + 4AE90E261DBEC10700865A75 /* banner_view.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1A1DBEC10700865A75 /* banner_view.cc */; }; + 4AE90E271DBEC10700865A75 /* interstitial_ad_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1B1DBEC10700865A75 /* interstitial_ad_internal.cc */; }; + 4AE90E281DBEC10700865A75 /* interstitial_ad.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1D1DBEC10700865A75 /* interstitial_ad.cc */; }; + 4AE90E291DBEC10700865A75 /* native_express_ad_view_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1E1DBEC10700865A75 /* native_express_ad_view_internal.cc */; }; + 4AE90E2A1DBEC10700865A75 /* native_express_ad_view.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E201DBEC10700865A75 /* native_express_ad_view.cc */; }; + 4AE90E2B1DBEC10700865A75 /* rewarded_video_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E211DBEC10700865A75 /* rewarded_video_internal.cc */; }; + 4AE90E2C1DBEC10700865A75 /* rewarded_video.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E231DBEC10700865A75 /* rewarded_video.cc */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 4AA541AF1CC6A3FE00973957 /* testapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = testapp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AA541CB1CC6A9B400973957 /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; }; + 4AD13E971CC9763C00AB0ACF /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = testapp/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + 4AD13E981CC9763C00AB0ACF /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = testapp/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + 4AD13E991CC9763C00AB0ACF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = testapp/Assets.xcassets; sourceTree = SOURCE_ROOT; }; + 4AD13E9F1CC9763C00AB0ACF /* game_engine.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = game_engine.cpp; path = testapp/game_engine.cpp; sourceTree = SOURCE_ROOT; }; + 4AD13EA01CC9763C00AB0ACF /* game_engine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = game_engine.h; path = testapp/game_engine.h; sourceTree = SOURCE_ROOT; }; + 4AD13EA11CC9763C00AB0ACF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = testapp/Info.plist; sourceTree = SOURCE_ROOT; }; + 4AD13EA21CC9763C00AB0ACF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = testapp/main.m; sourceTree = SOURCE_ROOT; }; + 4AD13EA31CC9763C00AB0ACF /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ViewController.h; path = testapp/ViewController.h; sourceTree = SOURCE_ROOT; }; + 4AD13EA41CC9763C00AB0ACF /* ViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ViewController.mm; path = testapp/ViewController.mm; sourceTree = SOURCE_ROOT; }; + 4AD13EAE1CC976C200AB0ACF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = testapp/Base.lproj/LaunchScreen.storyboard; sourceTree = SOURCE_ROOT; }; + 4AD13EB01CC976C200AB0ACF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = testapp/Base.lproj/Main.storyboard; sourceTree = SOURCE_ROOT; }; + 4AE90DED1DBEC0AA00865A75 /* log_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = log_ios.mm; path = ../../../../../../app/client/cpp/src/log_ios.mm; sourceTree = ""; }; + 4AE90DEF1DBEC0DC00865A75 /* log.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = log.cc; path = ../../../../../../app/client/cpp/src/log.cc; sourceTree = ""; }; + 4AE90DF01DBEC0DC00865A75 /* log.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = log.h; path = ../../../../../../app/client/cpp/src/log.h; sourceTree = ""; }; + 4AE90DF11DBEC0DC00865A75 /* mutex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = mutex.h; path = ../../../../../../app/client/cpp/src/mutex.h; sourceTree = ""; }; + 4AE90DF21DBEC0DC00865A75 /* reference_counted_future_impl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = reference_counted_future_impl.cc; path = ../../../../../../app/client/cpp/src/reference_counted_future_impl.cc; sourceTree = ""; }; + 4AE90DF31DBEC0DC00865A75 /* reference_counted_future_impl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = reference_counted_future_impl.h; path = ../../../../../../app/client/cpp/src/reference_counted_future_impl.h; sourceTree = ""; }; + 4AE90DF41DBEC0DC00865A75 /* util_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = util_ios.h; path = ../../../../../../app/client/cpp/src/util_ios.h; sourceTree = ""; }; + 4AE90DF51DBEC0DC00865A75 /* util_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = util_ios.mm; path = ../../../../../../app/client/cpp/src/util_ios.mm; sourceTree = ""; }; + 4AE90DF91DBEC0F300865A75 /* admob_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = admob_ios.mm; path = ../../../src/ios/admob_ios.mm; sourceTree = ""; }; + 4AE90DFA1DBEC0F300865A75 /* banner_view_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = banner_view_internal_ios.h; path = ../../../src/ios/banner_view_internal_ios.h; sourceTree = ""; }; + 4AE90DFB1DBEC0F300865A75 /* banner_view_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = banner_view_internal_ios.mm; path = ../../../src/ios/banner_view_internal_ios.mm; sourceTree = ""; }; + 4AE90DFC1DBEC0F300865A75 /* FADBannerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADBannerView.h; path = ../../../src/ios/FADBannerView.h; sourceTree = ""; }; + 4AE90DFD1DBEC0F300865A75 /* FADBannerView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADBannerView.mm; path = ../../../src/ios/FADBannerView.mm; sourceTree = ""; }; + 4AE90DFE1DBEC0F300865A75 /* FADInterstitialDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADInterstitialDelegate.h; path = ../../../src/ios/FADInterstitialDelegate.h; sourceTree = ""; }; + 4AE90DFF1DBEC0F300865A75 /* FADInterstitialDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADInterstitialDelegate.mm; path = ../../../src/ios/FADInterstitialDelegate.mm; sourceTree = ""; }; + 4AE90E001DBEC0F300865A75 /* FADNativeExpressAdView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADNativeExpressAdView.h; path = ../../../src/ios/FADNativeExpressAdView.h; sourceTree = ""; }; + 4AE90E011DBEC0F300865A75 /* FADNativeExpressAdView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADNativeExpressAdView.mm; path = ../../../src/ios/FADNativeExpressAdView.mm; sourceTree = ""; }; + 4AE90E021DBEC0F300865A75 /* FADRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADRequest.h; path = ../../../src/ios/FADRequest.h; sourceTree = ""; }; + 4AE90E031DBEC0F300865A75 /* FADRequest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADRequest.mm; path = ../../../src/ios/FADRequest.mm; sourceTree = ""; }; + 4AE90E041DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADRewardBasedVideoAdDelegate.h; path = ../../../src/ios/FADRewardBasedVideoAdDelegate.h; sourceTree = ""; }; + 4AE90E051DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADRewardBasedVideoAdDelegate.mm; path = ../../../src/ios/FADRewardBasedVideoAdDelegate.mm; sourceTree = ""; }; + 4AE90E061DBEC0F300865A75 /* interstitial_ad_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = interstitial_ad_internal_ios.h; path = ../../../src/ios/interstitial_ad_internal_ios.h; sourceTree = ""; }; + 4AE90E071DBEC0F300865A75 /* interstitial_ad_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = interstitial_ad_internal_ios.mm; path = ../../../src/ios/interstitial_ad_internal_ios.mm; sourceTree = ""; }; + 4AE90E081DBEC0F300865A75 /* native_express_ad_view_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = native_express_ad_view_internal_ios.h; path = ../../../src/ios/native_express_ad_view_internal_ios.h; sourceTree = ""; }; + 4AE90E091DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = native_express_ad_view_internal_ios.mm; path = ../../../src/ios/native_express_ad_view_internal_ios.mm; sourceTree = ""; }; + 4AE90E0A1DBEC0F300865A75 /* rewarded_video_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rewarded_video_internal_ios.h; path = ../../../src/ios/rewarded_video_internal_ios.h; sourceTree = ""; }; + 4AE90E0B1DBEC0F300865A75 /* rewarded_video_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = rewarded_video_internal_ios.mm; path = ../../../src/ios/rewarded_video_internal_ios.mm; sourceTree = ""; }; + 4AE90E161DBEC10700865A75 /* admob_common.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = admob_common.cc; path = ../../../src/common/admob_common.cc; sourceTree = ""; }; + 4AE90E171DBEC10700865A75 /* admob_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = admob_common.h; path = ../../../src/common/admob_common.h; sourceTree = ""; }; + 4AE90E181DBEC10700865A75 /* banner_view_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = banner_view_internal.cc; path = ../../../src/common/banner_view_internal.cc; sourceTree = ""; }; + 4AE90E191DBEC10700865A75 /* banner_view_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = banner_view_internal.h; path = ../../../src/common/banner_view_internal.h; sourceTree = ""; }; + 4AE90E1A1DBEC10700865A75 /* banner_view.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = banner_view.cc; path = ../../../src/common/banner_view.cc; sourceTree = ""; }; + 4AE90E1B1DBEC10700865A75 /* interstitial_ad_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = interstitial_ad_internal.cc; path = ../../../src/common/interstitial_ad_internal.cc; sourceTree = ""; }; + 4AE90E1C1DBEC10700865A75 /* interstitial_ad_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = interstitial_ad_internal.h; path = ../../../src/common/interstitial_ad_internal.h; sourceTree = ""; }; + 4AE90E1D1DBEC10700865A75 /* interstitial_ad.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = interstitial_ad.cc; path = ../../../src/common/interstitial_ad.cc; sourceTree = ""; }; + 4AE90E1E1DBEC10700865A75 /* native_express_ad_view_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = native_express_ad_view_internal.cc; path = ../../../src/common/native_express_ad_view_internal.cc; sourceTree = ""; }; + 4AE90E1F1DBEC10700865A75 /* native_express_ad_view_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = native_express_ad_view_internal.h; path = ../../../src/common/native_express_ad_view_internal.h; sourceTree = ""; }; + 4AE90E201DBEC10700865A75 /* native_express_ad_view.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = native_express_ad_view.cc; path = ../../../src/common/native_express_ad_view.cc; sourceTree = ""; }; + 4AE90E211DBEC10700865A75 /* rewarded_video_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = rewarded_video_internal.cc; path = ../../../src/common/rewarded_video_internal.cc; sourceTree = ""; }; + 4AE90E221DBEC10700865A75 /* rewarded_video_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rewarded_video_internal.h; path = ../../../src/common/rewarded_video_internal.h; sourceTree = ""; }; + 4AE90E231DBEC10700865A75 /* rewarded_video.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = rewarded_video.cc; path = ../../../src/common/rewarded_video.cc; sourceTree = ""; }; + 4AE90E2D1DBEC12000865A75 /* banner_view_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = banner_view_internal_stub.h; path = ../../../src/stub/banner_view_internal_stub.h; sourceTree = ""; }; + 4AE90E2E1DBEC12000865A75 /* interstitial_ad_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = interstitial_ad_internal_stub.h; path = ../../../src/stub/interstitial_ad_internal_stub.h; sourceTree = ""; }; + 4AE90E2F1DBEC12000865A75 /* native_express_ad_view_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = native_express_ad_view_internal_stub.h; path = ../../../src/stub/native_express_ad_view_internal_stub.h; sourceTree = ""; }; + 4AE90E301DBEC12000865A75 /* rewarded_video_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rewarded_video_internal_stub.h; path = ../../../src/stub/rewarded_video_internal_stub.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4AA541AC1CC6A3FE00973957 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AA541CC1CC6A9B400973957 /* GLKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4A1DEF141D0B27FC0002D14A /* common */ = { + isa = PBXGroup; + children = ( + 4AE90E161DBEC10700865A75 /* admob_common.cc */, + 4AE90E171DBEC10700865A75 /* admob_common.h */, + 4AE90E181DBEC10700865A75 /* banner_view_internal.cc */, + 4AE90E191DBEC10700865A75 /* banner_view_internal.h */, + 4AE90E1A1DBEC10700865A75 /* banner_view.cc */, + 4AE90E1B1DBEC10700865A75 /* interstitial_ad_internal.cc */, + 4AE90E1C1DBEC10700865A75 /* interstitial_ad_internal.h */, + 4AE90E1D1DBEC10700865A75 /* interstitial_ad.cc */, + 4AE90E1E1DBEC10700865A75 /* native_express_ad_view_internal.cc */, + 4AE90E1F1DBEC10700865A75 /* native_express_ad_view_internal.h */, + 4AE90E201DBEC10700865A75 /* native_express_ad_view.cc */, + 4AE90E211DBEC10700865A75 /* rewarded_video_internal.cc */, + 4AE90E221DBEC10700865A75 /* rewarded_video_internal.h */, + 4AE90E231DBEC10700865A75 /* rewarded_video.cc */, + ); + name = common; + sourceTree = ""; + }; + 4A1DEF151D0B28030002D14A /* ios */ = { + isa = PBXGroup; + children = ( + 4AE90DF91DBEC0F300865A75 /* admob_ios.mm */, + 4AE90DFA1DBEC0F300865A75 /* banner_view_internal_ios.h */, + 4AE90DFB1DBEC0F300865A75 /* banner_view_internal_ios.mm */, + 4AE90DFC1DBEC0F300865A75 /* FADBannerView.h */, + 4AE90DFD1DBEC0F300865A75 /* FADBannerView.mm */, + 4AE90DFE1DBEC0F300865A75 /* FADInterstitialDelegate.h */, + 4AE90DFF1DBEC0F300865A75 /* FADInterstitialDelegate.mm */, + 4AE90E001DBEC0F300865A75 /* FADNativeExpressAdView.h */, + 4AE90E011DBEC0F300865A75 /* FADNativeExpressAdView.mm */, + 4AE90E021DBEC0F300865A75 /* FADRequest.h */, + 4AE90E031DBEC0F300865A75 /* FADRequest.mm */, + 4AE90E041DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.h */, + 4AE90E051DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm */, + 4AE90E061DBEC0F300865A75 /* interstitial_ad_internal_ios.h */, + 4AE90E071DBEC0F300865A75 /* interstitial_ad_internal_ios.mm */, + 4AE90E081DBEC0F300865A75 /* native_express_ad_view_internal_ios.h */, + 4AE90E091DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm */, + 4AE90E0A1DBEC0F300865A75 /* rewarded_video_internal_ios.h */, + 4AE90E0B1DBEC0F300865A75 /* rewarded_video_internal_ios.mm */, + ); + name = ios; + sourceTree = ""; + }; + 4A242D501D45D5B500A98845 /* stub */ = { + isa = PBXGroup; + children = ( + 4AE90E2D1DBEC12000865A75 /* banner_view_internal_stub.h */, + 4AE90E2E1DBEC12000865A75 /* interstitial_ad_internal_stub.h */, + 4AE90E2F1DBEC12000865A75 /* native_express_ad_view_internal_stub.h */, + 4AE90E301DBEC12000865A75 /* rewarded_video_internal_stub.h */, + ); + name = stub; + sourceTree = ""; + }; + 4AA541A61CC6A3FE00973957 = { + isa = PBXGroup; + children = ( + 4AA542A41CC822BC00973957 /* firebase */, + 4AA5427F1CC6B70F00973957 /* firebase_admob */, + 4AA541B11CC6A3FE00973957 /* testapp */, + 4AA541C91CC6A5F500973957 /* Frameworks */, + 4AA541B01CC6A3FE00973957 /* Products */, + ); + sourceTree = ""; + }; + 4AA541B01CC6A3FE00973957 /* Products */ = { + isa = PBXGroup; + children = ( + 4AA541AF1CC6A3FE00973957 /* testapp.app */, + ); + name = Products; + sourceTree = ""; + }; + 4AA541B11CC6A3FE00973957 /* testapp */ = { + isa = PBXGroup; + children = ( + 4AD13E971CC9763C00AB0ACF /* AppDelegate.h */, + 4AD13E981CC9763C00AB0ACF /* AppDelegate.m */, + 4AD13EA31CC9763C00AB0ACF /* ViewController.h */, + 4AD13EA41CC9763C00AB0ACF /* ViewController.mm */, + 4AD13EA01CC9763C00AB0ACF /* game_engine.h */, + 4AD13E9F1CC9763C00AB0ACF /* game_engine.cpp */, + 4AD13E991CC9763C00AB0ACF /* Assets.xcassets */, + 4AD13EAD1CC976C200AB0ACF /* LaunchScreen.storyboard */, + 4AD13EAF1CC976C200AB0ACF /* Main.storyboard */, + 4AD13EA11CC9763C00AB0ACF /* Info.plist */, + 4AA541B21CC6A3FE00973957 /* Supporting Files */, + ); + path = testapp; + sourceTree = ""; + }; + 4AA541B21CC6A3FE00973957 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4AD13EA21CC9763C00AB0ACF /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 4AA541C91CC6A5F500973957 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4AA541CB1CC6A9B400973957 /* GLKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4AA5427F1CC6B70F00973957 /* firebase_admob */ = { + isa = PBXGroup; + children = ( + 4A1DEF151D0B28030002D14A /* ios */, + 4A1DEF141D0B27FC0002D14A /* common */, + 4A242D501D45D5B500A98845 /* stub */, + ); + name = firebase_admob; + sourceTree = ""; + }; + 4AA542A41CC822BC00973957 /* firebase */ = { + isa = PBXGroup; + children = ( + 4AE90DEF1DBEC0DC00865A75 /* log.cc */, + 4AE90DF01DBEC0DC00865A75 /* log.h */, + 4AE90DF11DBEC0DC00865A75 /* mutex.h */, + 4AE90DF21DBEC0DC00865A75 /* reference_counted_future_impl.cc */, + 4AE90DF31DBEC0DC00865A75 /* reference_counted_future_impl.h */, + 4AE90DF41DBEC0DC00865A75 /* util_ios.h */, + 4AE90DF51DBEC0DC00865A75 /* util_ios.mm */, + 4AE90DED1DBEC0AA00865A75 /* log_ios.mm */, + ); + name = firebase; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4AA541AE1CC6A3FE00973957 /* testapp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AA541C61CC6A3FE00973957 /* Build configuration list for PBXNativeTarget "testapp" */; + buildPhases = ( + 4AA541AB1CC6A3FE00973957 /* Sources */, + 4AA541AC1CC6A3FE00973957 /* Frameworks */, + 4AA541AD1CC6A3FE00973957 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = testapp; + productName = testapp; + productReference = 4AA541AF1CC6A3FE00973957 /* testapp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4AA541A71CC6A3FE00973957 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 4AA541AE1CC6A3FE00973957 = { + CreatedOnToolsVersion = 7.3; + }; + }; + }; + buildConfigurationList = 4AA541AA1CC6A3FE00973957 /* Build configuration list for PBXProject "testapp" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4AA541A61CC6A3FE00973957; + productRefGroup = 4AA541B01CC6A3FE00973957 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4AA541AE1CC6A3FE00973957 /* testapp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4AA541AD1CC6A3FE00973957 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AD13EB11CC976C200AB0ACF /* LaunchScreen.storyboard in Resources */, + 4AD13EA61CC9763C00AB0ACF /* Assets.xcassets in Resources */, + 4AD13EB21CC976C200AB0ACF /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4AA541AB1CC6A3FE00973957 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AE90DF71DBEC0DC00865A75 /* reference_counted_future_impl.cc in Sources */, + 4AE90E291DBEC10700865A75 /* native_express_ad_view_internal.cc in Sources */, + 4AE90DF61DBEC0DC00865A75 /* log.cc in Sources */, + 4AE90E2A1DBEC10700865A75 /* native_express_ad_view.cc in Sources */, + 4AE90E281DBEC10700865A75 /* interstitial_ad.cc in Sources */, + 4AE90E151DBEC0F300865A75 /* rewarded_video_internal_ios.mm in Sources */, + 4AE90DEE1DBEC0AA00865A75 /* log_ios.mm in Sources */, + 4AE90DF81DBEC0DC00865A75 /* util_ios.mm in Sources */, + 4AD13EA51CC9763C00AB0ACF /* AppDelegate.m in Sources */, + 4AE90E2C1DBEC10700865A75 /* rewarded_video.cc in Sources */, + 4AE90E261DBEC10700865A75 /* banner_view.cc in Sources */, + 4AE90E241DBEC10700865A75 /* admob_common.cc in Sources */, + 4AE90E271DBEC10700865A75 /* interstitial_ad_internal.cc in Sources */, + 4AE90E0D1DBEC0F300865A75 /* banner_view_internal_ios.mm in Sources */, + 4AD13EAC1CC9763C00AB0ACF /* ViewController.mm in Sources */, + 4AE90E0E1DBEC0F300865A75 /* FADBannerView.mm in Sources */, + 4AE90E2B1DBEC10700865A75 /* rewarded_video_internal.cc in Sources */, + 4AD13EA91CC9763C00AB0ACF /* game_engine.cpp in Sources */, + 4AE90E111DBEC0F300865A75 /* FADRequest.mm in Sources */, + 4AE90E0F1DBEC0F300865A75 /* FADInterstitialDelegate.mm in Sources */, + 4AE90E131DBEC0F300865A75 /* interstitial_ad_internal_ios.mm in Sources */, + 4AE90E121DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm in Sources */, + 4AE90E251DBEC10700865A75 /* banner_view_internal.cc in Sources */, + 4AE90E0C1DBEC0F300865A75 /* admob_ios.mm in Sources */, + 4AE90E101DBEC0F300865A75 /* FADNativeExpressAdView.mm in Sources */, + 4AD13EAB1CC9763C00AB0ACF /* main.m in Sources */, + 4AE90E141DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 4AD13EAD1CC976C200AB0ACF /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4AD13EAE1CC976C200AB0ACF /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 4AD13EAF1CC976C200AB0ACF /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4AD13EB01CC976C200AB0ACF /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4AA541C41CC6A3FE00973957 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + "-Wswitch-enum", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(SRCROOT)//../../../../../../../ $(SRCROOT)/../../../../../../../firebase/admob/client/cpp/src $(SRCROOT)//../../../../../../../firebase/admob/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include/firebase"; + }; + name = Debug; + }; + 4AA541C51CC6A3FE00973957 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-Wswitch-enum", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(SRCROOT)//../../../../../../../ $(SRCROOT)/../../../../../../../firebase/admob/client/cpp/src $(SRCROOT)//../../../../../../../firebase/admob/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include/firebase"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4AA541C71CC6A3FE00973957 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImages; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = testapp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.ios.admob.testapp; + PRODUCT_NAME = testapp; + }; + name = Debug; + }; + 4AA541C81CC6A3FE00973957 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImages; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = testapp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.ios.admob.testapp; + PRODUCT_NAME = testapp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4AA541AA1CC6A3FE00973957 /* Build configuration list for PBXProject "testapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AA541C41CC6A3FE00973957 /* Debug */, + 4AA541C51CC6A3FE00973957 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AA541C61CC6A3FE00973957 /* Build configuration list for PBXNativeTarget "testapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AA541C71CC6A3FE00973957 /* Debug */, + 4AA541C81CC6A3FE00973957 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4AA541A71CC6A3FE00973957 /* Project object */; +} diff --git a/admob/tools/ios/testapp/testapp/AppDelegate.h b/admob/tools/ios/testapp/testapp/AppDelegate.h new file mode 100644 index 0000000000..2701a9510d --- /dev/null +++ b/admob/tools/ios/testapp/testapp/AppDelegate.h @@ -0,0 +1,11 @@ +// Copyright © 2016 Google. All rights reserved. + +@import GoogleMobileAds; + +#import + +@interface AppDelegate : UIResponder + +@property(nonatomic, strong) UIWindow *window; + +@end diff --git a/admob/tools/ios/testapp/testapp/AppDelegate.m b/admob/tools/ios/testapp/testapp/AppDelegate.m new file mode 100644 index 0000000000..1c561de93d --- /dev/null +++ b/admob/tools/ios/testapp/testapp/AppDelegate.m @@ -0,0 +1,16 @@ +// Copyright © 2016 Google. All rights reserved. + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + return YES; +} + +@end diff --git a/admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json b/admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..eeea76c2db --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json b/admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json b/admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json new file mode 100644 index 0000000000..a0ad363c85 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard b/admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..94b4751591 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard b/admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..98dfe0f664 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admob/tools/ios/testapp/testapp/Info.plist b/admob/tools/ios/testapp/testapp/Info.plist new file mode 100644 index 0000000000..1bcde66117 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Info.plist @@ -0,0 +1,50 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/admob/tools/ios/testapp/testapp/ViewController.h b/admob/tools/ios/testapp/testapp/ViewController.h new file mode 100644 index 0000000000..5c0b4cd70f --- /dev/null +++ b/admob/tools/ios/testapp/testapp/ViewController.h @@ -0,0 +1,8 @@ +// Copyright © 2016 Google. All rights reserved. + +#import +#import + +@interface ViewController : UIViewController + +@end diff --git a/admob/tools/ios/testapp/testapp/ViewController.mm b/admob/tools/ios/testapp/testapp/ViewController.mm new file mode 100644 index 0000000000..fe9446ac0b --- /dev/null +++ b/admob/tools/ios/testapp/testapp/ViewController.mm @@ -0,0 +1,135 @@ +// Copyright © 2016 Google. All rights reserved. + +#import "admob/tools/ios/testapp/testapp/ViewController.h" + +#import "admob/tools/ios/testapp/testapp/game_engine.h" + +@interface ViewController () { + /// The AdMob C++ Wrapper Game Engine. + GameEngine *_gameEngine; + + /// The GLKView provides a default implementation of an OpenGL ES view. + GLKView *_glkView; +} + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Set up the GLKView. + _glkView = [[GLKView alloc] init]; + [self setUpLayoutConstraintsForGLKView]; + [self.view addSubview:_glkView]; + + [self setUpGL]; +} + +#pragma mark - GLKView Setup Methods + +- (void)setUpGL { + // Set the OpenGL ES rendering context. + _glkView.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + [EAGLContext setCurrentContext:_glkView.context]; + _glkView.drawableDepthFormat = GLKViewDrawableDepthFormat24; + _glkView.delegate = self; + + // Allocate and initialize a GLKViewController to implement an OpenGL ES rendering loop. + GLKViewController *viewController = [[GLKViewController alloc] initWithNibName:nil bundle:nil]; + viewController.view = _glkView; + viewController.delegate = self; + [self addChildViewController:viewController]; + + // Create a C++ GameEngine object and call the set up methods. + _gameEngine = new GameEngine(); + self->_gameEngine->Initialize(self.view); + self->_gameEngine->onSurfaceCreated(); + // Making the assumption that the glkView is equal to the mainScreen size. In other words, the + // glkView is full screen. + CGSize screenSize = [[[UIScreen mainScreen] currentMode] size]; + self->_gameEngine->onSurfaceChanged(screenSize.width, screenSize.height); + + // Set up the UITapGestureRecognizer for the GLKView. + UITapGestureRecognizer *tapRecognizer = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; + tapRecognizer.numberOfTapsRequired = 1; + [self.view addGestureRecognizer:tapRecognizer]; +} + +- (void)setUpLayoutConstraintsForGLKView { + [self.view addSubview:_glkView]; + _glkView.translatesAutoresizingMaskIntoConstraints = NO; + + // Layout constraints that match the parent view. + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeHeight + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeWidth + multiplier:1 + constant:0]]; +} + +#pragma mark - GLKViewDelegate + +- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { + self->_gameEngine->onDrawFrame(); +} + +#pragma mark - GLKViewController + +- (void)glkViewControllerUpdate:(GLKViewController *)controller { + self->_gameEngine->onUpdate(); +} + +#pragma mark - Actions + +- (void)handleTap:(UITapGestureRecognizer *)recognizer { + if (recognizer.state == UIGestureRecognizerStateEnded) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + CGFloat scale = [[UIScreen mainScreen] scale]; + CGPoint tapLocation = [recognizer locationInView:self->_glkView]; + // Map the x and y coordinates to pixel values using the scale factor associated with the + // device's screen. + int scaledX = tapLocation.x * scale; + int scaledY = tapLocation.y * scale; + self->_gameEngine->onTap(scaledX, scaledY); + }); + } +} + +#pragma mark - Log Message + +// Log a message that can be viewed in the console. +int LogMessage(const char* format, ...) { + va_list list; + int rc = 0; + va_start(list, format); + NSLogv(@(format), list); + va_end(list); + return rc; +} + +@end diff --git a/admob/tools/ios/testapp/testapp/game_engine.cpp b/admob/tools/ios/testapp/testapp/game_engine.cpp new file mode 100644 index 0000000000..270f90c688 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/game_engine.cpp @@ -0,0 +1,551 @@ +// Copyright © 2016 Google. All rights reserved. + +#include "admob/tools/ios/testapp/testapp/game_engine.h" + +#include + +#include "app/src/assert.h" + +namespace rewarded_video = firebase::admob::rewarded_video; + +// AdMob app ID. +const char* kAdMobAppID = "ca-app-pub-3940256099942544~1458002511"; + +// AdMob ad unit IDs. +const char* kBannerAdUnit = "ca-app-pub-3940256099942544/2934735716"; +const char* kNativeExpressAdUnit = "ca-app-pub-3940256099942544/2562852117"; +const char* kInterstitialAdUnit = "ca-app-pub-3940256099942544/4411468910"; +const char* kRewardedVideoAdUnit = "ca-app-pub-2618531387707574/6671583249"; + +// A simple listener that logs changes to a BannerView. +class LoggingBannerViewListener : public firebase::admob::BannerView::Listener { + public: + LoggingBannerViewListener() {} + void OnPresentationStateChanged( + firebase::admob::BannerView* banner_view, + firebase::admob::BannerView::PresentationState state) override { + LogMessage("BannerView PresentationState has changed to %d.", state); + } + void OnBoundingBoxChanged(firebase::admob::BannerView* banner_view, + firebase::admob::BoundingBox box) override { + LogMessage( + "BannerView BoundingBox has changed to (x: %d, y: %d, width: %d, " + "height %d)", + box.x, box.y, box.width, box.height); + } +}; + +// A simple listener that logs changes to a NativeExpressAdView. +class LoggingNativeExpressAdViewListener + : public firebase::admob::NativeExpressAdView::Listener { + public: + LoggingNativeExpressAdViewListener() {} + void OnPresentationStateChanged( + firebase::admob::NativeExpressAdView* native_express_view, + firebase::admob::NativeExpressAdView::PresentationState state) override { + LogMessage("NativeExpressAdView PresentationState has changed to %d.", + state); + } + void OnBoundingBoxChanged( + firebase::admob::NativeExpressAdView* native_express_view, + firebase::admob::BoundingBox box) override { + LogMessage( + "NativeExpressAd BoundingBox has changed to (x: %d, y: %d, width: %d, " + "height %d)", + box.x, box.y, box.width, box.height); + } +}; + +// A simple listener that logs changes to an InterstitialAd. +class LoggingInterstitialAdListener + : public firebase::admob::InterstitialAd::Listener { + public: + LoggingInterstitialAdListener() {} + void OnPresentationStateChanged( + firebase::admob::InterstitialAd* interstitial_ad, + firebase::admob::InterstitialAd::PresentationState state) override { + LogMessage("InterstitialAd PresentationState has changed to %d.", state); + } +}; + +// A simple listener that logs changes to rewarded video state. +class LoggingRewardedVideoListener : public rewarded_video::Listener { + public: + LoggingRewardedVideoListener() {} + void OnRewarded(rewarded_video::RewardItem reward) override { + LogMessage("Reward user with %f %s.", reward.amount, + reward.reward_type.c_str()); + } + void OnPresentationStateChanged( + rewarded_video::PresentationState state) override { + LogMessage("Rewarded video PresentationState has changed to %d.", state); + } +}; + +// The listeners for logging changes to the AdMob ad formats. +LoggingBannerViewListener banner_listener; +LoggingNativeExpressAdViewListener native_express_listener; +LoggingInterstitialAdListener interstitial_listener; +LoggingRewardedVideoListener rewarded_listener; + +// GameEngine constructor. +GameEngine::GameEngine() {} + +// Sets up AdMob C++. +void GameEngine::Initialize(firebase::admob::AdParent ad_parent) { + FIREBASE_ASSERT(kTestBannerView != kTestNativeExpressAdView && + "kTestBannerView and kTestNativeExpressAdView cannot both be " + "true/false at the same time."); + FIREBASE_ASSERT(kTestInterstitialAd != kTestRewardedVideo && + "kTestInterstitialAd and kTestRewardedVideo cannot both be " + "true/false at the same time."); + + firebase::admob::Initialize(kAdMobAppID); + parent_view_ = ad_parent; + + if (kTestBannerView) { + // Create an ad size and initialize the BannerView. + firebase::admob::AdSize bannerAdSize; + bannerAdSize.width = 320; + bannerAdSize.height = 50; + banner_view_ = new firebase::admob::BannerView(); + banner_view_->Initialize(parent_view_, kBannerAdUnit, bannerAdSize); + banner_view_listener_set_ = false; + } + + if (kTestNativeExpressAdView) { + // Create an ad size and initialize the NativeExpressAdView. + firebase::admob::AdSize nativeExpressAdSize; + nativeExpressAdSize.width = 320; + nativeExpressAdSize.height = 220; + native_express_view_ = new firebase::admob::NativeExpressAdView(); + native_express_view_->Initialize(parent_view_, kNativeExpressAdUnit, + nativeExpressAdSize); + native_express_ad_view_listener_set_ = false; + } + + if (kTestInterstitialAd) { + // Initialize the InterstitialAd. + interstitial_ad_ = new firebase::admob::InterstitialAd(); + interstitial_ad_->Initialize(parent_view_, kInterstitialAdUnit); + interstitial_ad_listener_set_ = false; + } + + if (kTestRewardedVideo) { + // Initialize the rewarded_video:: namespace. + rewarded_video::Initialize(); + // If you want to poll the reward, uncomment the poll_listener_ code in the + // update() function. When the poll_listener_code is commented out in + // update(), then the LoggingRewardedVideoListener is used to log changes to + // the rewarded video state. + poll_listener_ = nullptr; + rewarded_video_listener_set_ = false; + } +} + +// Creates the AdMob C++ ad request. +firebase::admob::AdRequest GameEngine::createRequest() { + // Sample keywords to use in making the request. + static const char* kKeywords[] = {"AdMob", "C++", "Fun"}; + + // Sample test device IDs to use in making the request. + static const char* kTestDeviceIDs[] = {"2077ef9a63d2b398840261c8221a0c9b", + "098fe087d987c9a878965454a65654d7"}; + + // Sample birthday value to use in making the request. + static const int kBirthdayDay = 10; + static const int kBirthdayMonth = 11; + static const int kBirthdayYear = 1976; + + firebase::admob::AdRequest request; + request.gender = firebase::admob::kGenderUnknown; + + request.tagged_for_child_directed_treatment = + firebase::admob::kChildDirectedTreatmentStateTagged; + + request.birthday_day = kBirthdayDay; + request.birthday_month = kBirthdayMonth; + request.birthday_year = kBirthdayYear; + + request.keyword_count = sizeof(kKeywords) / sizeof(kKeywords[0]); + request.keywords = kKeywords; + + static const firebase::admob::KeyValuePair kRequestExtras[] = { + {"the_name_of_an_extra", "the_value_for_that_extra"}}; + request.extras_count = sizeof(kRequestExtras) / sizeof(kRequestExtras[0]); + request.extras = kRequestExtras; + + request.test_device_id_count = + sizeof(kTestDeviceIDs) / sizeof(kTestDeviceIDs[0]); + request.test_device_ids = kTestDeviceIDs; + + return request; +} + +// Updates the game engine (game loop). +void GameEngine::onUpdate() { + if (kTestBannerView) { + // Set the banner view listener. + if (banner_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !banner_view_listener_set_) { + banner_view_->SetListener(&banner_listener); + banner_view_listener_set_ = true; + } + } + + if (kTestNativeExpressAdView) { + // Set the native express ad view listener. + if (native_express_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !native_express_ad_view_listener_set_) { + native_express_view_->SetListener(&native_express_listener); + native_express_ad_view_listener_set_ = true; + } + } + + if (kTestInterstitialAd) { + // Set the interstitial ad listener. + if (interstitial_ad_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !interstitial_ad_listener_set_) { + interstitial_ad_->SetListener(&interstitial_listener); + interstitial_ad_listener_set_ = true; + } + + // Once the interstitial ad has been displayed to and dismissed by the user, + // create a new interstitial ad. + if (interstitial_ad_->ShowLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->ShowLastResult().Error() == + firebase::admob::kAdMobErrorNone && + interstitial_ad_->GetPresentationState() == + firebase::admob::InterstitialAd::kPresentationStateHidden) { + delete interstitial_ad_; + interstitial_ad_ = nullptr; + interstitial_ad_ = new firebase::admob::InterstitialAd(); + interstitial_ad_->Initialize(parent_view_, kInterstitialAdUnit); + interstitial_ad_listener_set_ = false; + } + } + + if (kTestRewardedVideo) { + // Set the rewarded video listener. + if (rewarded_video::InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !rewarded_video_listener_set_) { + // && poll_listener == nullptr) { + rewarded_video::SetListener(&rewarded_listener); + rewarded_video_listener_set_ = true; + // poll_listener_ = new + // firebase::admob::rewarded_video::PollableRewardListener(); + // rewarded_video::SetListener(poll_listener_); + } + + // Once the rewarded video ad has been displayed to and dismissed by the + // user, create a new rewarded video ad. + if (rewarded_video::ShowLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::ShowLastResult().Error() == + firebase::admob::kAdMobErrorNone && + rewarded_video::GetPresentationState() == + firebase::admob::rewarded_video::kPresentationStateHidden) { + rewarded_video::Destroy(); + rewarded_video::Initialize(); + rewarded_video_listener_set_ = false; + } + } + + // Increment red if increasing, decrement otherwise. + float diff = bg_intensity_increasing_ ? 0.0025f : -0.0025f; + + // Increment red up to 1.0, then back down to 0.0, repeat. + bg_intensity_ += diff; + if (bg_intensity_ >= 0.4f) { + bg_intensity_increasing_ = false; + } else if (bg_intensity_ <= 0.0f) { + bg_intensity_increasing_ = true; + } +} + +// Handles user tapping on one of the kNumberOfButtons. +void GameEngine::onTap(float x, float y) { + int button_number = -1; + GLfloat viewport_x = 1 - (((width_ - x) * 2) / width_); + GLfloat viewport_y = 1 - (((y)*2) / height_); + + for (int i = 0; i < kNumberOfButtons; i++) { + if ((viewport_x >= vertices_[i * 8]) && + (viewport_x <= vertices_[i * 8 + 2]) && + (viewport_y <= vertices_[i * 8 + 1]) && + (viewport_y >= vertices_[i * 8 + 5])) { + button_number = i; + break; + } + } + + // The BannerView or NativeExpressAdView's bounding box. + firebase::admob::BoundingBox box; + + switch (button_number) { + case 0: + if (kTestBannerView) { + // Load the banner ad. + if (banner_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + banner_view_->LoadAd(createRequest()); + } + } + if (kTestNativeExpressAdView) { + // Load the native express ad. + if (native_express_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + native_express_view_->LoadAd(createRequest()); + } + } + break; + case 1: + if (kTestBannerView) { + // Show/Hide the BannerView. + if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + banner_view_->GetPresentationState() == + firebase::admob::BannerView::kPresentationStateHidden) { + banner_view_->Show(); + } else if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->GetPresentationState() == + firebase::admob::BannerView:: + kPresentationStateVisibleWithAd) { + banner_view_->Hide(); + } + } + if (kTestNativeExpressAdView) { + // Show/Hide the NativeExpressAdView. + if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + native_express_view_->GetPresentationState() == + firebase::admob::NativeExpressAdView:: + kPresentationStateHidden) { + native_express_view_->Show(); + } else if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + native_express_view_->GetPresentationState() == + firebase::admob::NativeExpressAdView:: + kPresentationStateVisibleWithAd) { + native_express_view_->Hide(); + } + } + break; + case 2: + if (kTestBannerView) { + // Move the BannerView to a predefined position. + if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + banner_view_->MoveTo(firebase::admob::BannerView::kPositionBottom); + } + } + if (kTestNativeExpressAdView) { + // Move the NativeExpressAdView to a predefined position. + if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + native_express_view_->MoveTo( + firebase::admob::NativeExpressAdView::kPositionBottom); + } + } + break; + case 3: + if (kTestBannerView) { + // Move the BannerView to a specific x and y coordinate. + if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + int x = 100; + int y = 200; + banner_view_->MoveTo(x, y); + } + } + if (kTestNativeExpressAdView) { + // Move the NativeExpressAdView to a specific x and y coordinate. + if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + int x = 100; + int y = 200; + native_express_view_->MoveTo(x, y); + } + } + if (kTestRewardedVideo) { + // Poll the reward. + if (poll_listener_ != nullptr) { + while (poll_listener_->PollReward(&reward_)) { + LogMessage("Reward user with %f %s.", reward_.amount, + reward_.reward_type.c_str()); + } + } + } + break; + case 4: + if (kTestInterstitialAd) { + // Load the interstitial ad. + if (interstitial_ad_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + interstitial_ad_->LoadAd(createRequest()); + } + } + if (kTestRewardedVideo) { + // Load the rewarded video ad. + if (rewarded_video::InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + rewarded_video::LoadAd(kRewardedVideoAdUnit, createRequest()); + } + } + break; + case 5: + if (kTestInterstitialAd) { + // Show the interstitial ad. + if (interstitial_ad_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + interstitial_ad_->ShowLastResult().Status() != + firebase::kFutureStatusComplete) { + interstitial_ad_->Show(); + } + } + if (kTestRewardedVideo) { + // Show the rewarded video ad. + if (rewarded_video::LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + rewarded_video::ShowLastResult().Status() != + firebase::kFutureStatusComplete) { + rewarded_video::Show(parent_view_); + } + } + break; + default: + break; + } +} + +// The vertex shader code string. +static const GLchar* kVertexShaderCodeString = + "attribute vec2 position;\n" + "\n" + "void main()\n" + "{\n" + " gl_Position = vec4(position, 0.0, 1.0);\n" + "}"; + +// The fragment shader code string. +static const GLchar* kFragmentShaderCodeString = + "precision mediump float;\n" + "uniform vec4 myColor; \n" + "void main() { \n" + " gl_FragColor = myColor; \n" + "}"; + +// Creates the OpenGL surface. +void GameEngine::onSurfaceCreated() { + vertex_shader_ = glCreateShader(GL_VERTEX_SHADER); + fragment_shader_ = glCreateShader(GL_FRAGMENT_SHADER); + + glShaderSource(vertex_shader_, 1, &kVertexShaderCodeString, NULL); + glCompileShader(vertex_shader_); + + GLint status; + glGetShaderiv(vertex_shader_, GL_COMPILE_STATUS, &status); + + char buffer[512]; + glGetShaderInfoLog(vertex_shader_, 512, NULL, buffer); + + glShaderSource(fragment_shader_, 1, &kFragmentShaderCodeString, NULL); + glCompileShader(fragment_shader_); + + glGetShaderiv(fragment_shader_, GL_COMPILE_STATUS, &status); + + glGetShaderInfoLog(fragment_shader_, 512, NULL, buffer); + + shader_program_ = glCreateProgram(); + glAttachShader(shader_program_, vertex_shader_); + glAttachShader(shader_program_, fragment_shader_); + + glLinkProgram(shader_program_); + glUseProgram(shader_program_); +} + +// Updates the OpenGL surface. +void GameEngine::onSurfaceChanged(int width, int height) { + width_ = width; + height_ = height; + + GLfloat heightIncrement = 0.25f; + GLfloat currentHeight = 0.93f; + + for (int i = 0; i < kNumberOfButtons; i++) { + int base = i * 8; + vertices_[base] = -0.9f; + vertices_[base + 1] = currentHeight; + vertices_[base + 2] = 0.9f; + vertices_[base + 3] = currentHeight; + vertices_[base + 4] = -0.9f; + vertices_[base + 5] = currentHeight - heightIncrement; + vertices_[base + 6] = 0.9f; + vertices_[base + 7] = currentHeight - heightIncrement; + currentHeight -= 1.2 * heightIncrement; + } +} + +// Draws the frame for the OpenGL surface. +void GameEngine::onDrawFrame() { + glClearColor(0.0f, 0.0f, bg_intensity_, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + GLuint vbo; + glGenBuffers(1, &vbo); + + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_), vertices_, GL_STATIC_DRAW); + + GLfloat colorBytes[] = {0.9f, 0.9f, 0.9f, 1.0f}; + GLint colorLocation = glGetUniformLocation(shader_program_, "myColor"); + glUniform4fv(colorLocation, 1, colorBytes); + + GLint posAttrib = glGetAttribLocation(shader_program_, "position"); + glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(posAttrib); + + for (int i = 0; i < kNumberOfButtons; i++) { + glDrawArrays(GL_TRIANGLE_STRIP, i * 4, 4); + } +} diff --git a/admob/tools/ios/testapp/testapp/game_engine.h b/admob/tools/ios/testapp/testapp/game_engine.h new file mode 100644 index 0000000000..7e3f062954 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/game_engine.h @@ -0,0 +1,74 @@ +// Copyright © 2016 Google. All rights reserved. + +#ifndef GAME_ENGINE_H_ +#define GAME_ENGINE_H_ + +#include +#include + +#include "firebase/admob.h" +#include "firebase/admob/banner_view.h" +#include "firebase/admob/interstitial_ad.h" +#include "firebase/admob/native_express_ad_view.h" +#include "firebase/admob/rewarded_video.h" +#include "firebase/admob/types.h" + +#ifndef __cplusplus +#error Header file supports C++ only +#endif // __cplusplus + +// Cross platform logging method. +extern "C" int LogMessage(const char* format, ...); + +class GameEngine { + static const int kNumberOfButtons = 6; + + // Set these flags to enable the ad formats that you want to test. + // BannerView and NativeExpressAdView share the same buttons for this testapp, + // so only one of these flags can be set to true when running the app. + static const bool kTestBannerView = true; + static const bool kTestNativeExpressAdView = false; + // InterstitialAd and rewarded_video:: share the same buttons for this + // testapp, so only one of these flags can be set to true when running the + // app. + static const bool kTestInterstitialAd = true; + static const bool kTestRewardedVideo = false; + + public: + GameEngine(); + + void Initialize(firebase::admob::AdParent ad_parent); + void onUpdate(); + void onTap(float x, float y); + void onSurfaceCreated(); + void onSurfaceChanged(int width, int height); + void onDrawFrame(); + + private: + firebase::admob::AdRequest createRequest(); + + firebase::admob::BannerView* banner_view_; + firebase::admob::NativeExpressAdView* native_express_view_; + firebase::admob::InterstitialAd* interstitial_ad_; + + bool banner_view_listener_set_; + bool native_express_ad_view_listener_set_; + bool interstitial_ad_listener_set_; + bool rewarded_video_listener_set_; + + firebase::admob::AdParent parent_view_; + firebase::admob::rewarded_video::PollableRewardListener* poll_listener_; + firebase::admob::rewarded_video::RewardItem reward_; + + bool bg_intensity_increasing_; + float bg_intensity_; + + GLuint vertex_shader_; + GLuint fragment_shader_; + GLuint shader_program_; + int height_; + int width_; + GLfloat vertices_[kNumberOfButtons * 8]; +}; + +#endif // GAME_ENGINE_H_ diff --git a/admob/tools/ios/testapp/testapp/main.m b/admob/tools/ios/testapp/testapp/main.m new file mode 100644 index 0000000000..35f5ab1db6 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/main.m @@ -0,0 +1,11 @@ +// Copyright © 2016 Google. All rights reserved. + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } + +} diff --git a/analytics/generate_constants_test.py b/analytics/generate_constants_test.py new file mode 100644 index 0000000000..380b300a28 --- /dev/null +++ b/analytics/generate_constants_test.py @@ -0,0 +1,176 @@ +# Copyright 2016 Google LLC +# +# 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. + +"""Tests for generate_constants_lib.py.""" + +import datetime + +from google3.testing.pybase import googletest +from google3.firebase.analytics.client.cpp import generate_constants_lib + + +class GenerateHeaderTest(googletest.TestCase): + """Tests functions used to generate C++ header boilerplate.""" + + def test_cpp_header_guard(self): + """Verify header guards are formatted correctly.""" + self.assertEqual( + 'SOME_API_CPP_MYAPI_H_', + generate_constants_lib.cpp_header_guard('SOME_API_CPP_', 'myapi')) + + def test_format_cpp_header_header(self): + """Verify the header of C++ headers are formatted correctly.""" + self.assertEqual( + '// Copyright %s Google Inc. All Rights Reserved.\n' + '\n' + '#ifndef SOME_API_CPP_MYAPI_H_\n' + '#define SOME_API_CPP_MYAPI_H_\n' + '\n' + '/// @brief my package docs\n' + 'namespace mypackage {\n' + '/// @brief my api docs\n' + 'namespace myapi {\n' + '\n' % str(datetime.date.today().year), + generate_constants_lib.format_cpp_header_header( + 'SOME_API_CPP_', 'myapi.h', [('mypackage', 'my package docs'), + ('myapi', 'my api docs')])) + + def test_format_cpp_header_footer(self): + """Verify the footer of C++ headers are formatted correctly.""" + self.assertEqual( + '\n' + '} // namespace myapi\n' + '} // namespace mypackage\n' + '\n' + '#endif // SOME_API_CPP_MYAPI_H_\n', + generate_constants_lib.format_cpp_header_footer('SOME_API_CPP_', + 'myapi.h', + ['mypackage', 'myapi'])) + + +class DocStringParserTest(googletest.TestCase): + """Tests for DocStringParser.""" + + def test_parse_line(self): + """Test successfully parsing a line.""" + parser = generate_constants_lib.DocStringParser() + doc_line = '/// This is a test' + self.assertTrue(parser.parse_line(doc_line)) + self.assertListEqual([doc_line], parser.doc_string_lines) + + def test_parse_line_no_docs(self): + """Verify lines that don't contain docs are not parsed.""" + parser = generate_constants_lib.DocStringParser() + self.assertFalse(parser.parse_line( + 'static NSString *const test = @"test";')) + self.assertListEqual([], parser.doc_string_lines) + + def test_reset(self): + """Verify it's possible to reset the state of the parser.""" + parser = generate_constants_lib.DocStringParser() + self.assertTrue(parser.parse_line('/// This is a test')) + parser.reset() + self.assertListEqual([], parser.doc_string_lines) + + def test_apply_replacements(self): + """Test transformation of parsed doc strings.""" + parser = generate_constants_lib.DocStringParser(replacements=( + ('kT.XBish', 'kBish'), ('Bosh', ''), ('yo', 'hey'))) + self.assertEqual( + '/// This is a test of kBish', + generate_constants_lib.DocStringParser.apply_replacements( + '/// This is a test of kTTXBishBosh', + parser.replacements)) + + self.assertEqual( + '/// This is a hey of kBish hey', + generate_constants_lib.DocStringParser.apply_replacements( + '/// This is a yo of kTTXBishBosh yo', + parser.replacements, + replace_multiple_times=True)) + + def test_wrap_lines(self): + """Test line wrapping of parsed doc strings.""" + parser = generate_constants_lib.DocStringParser() + wrapped_lines = parser.wrap_lines( + ['/// this is a short paragraph', + '///', + '/// this is a' + (' very long line' * 10), + '///', + '/// more content', + '/// and some html that should not be wrapped', + '///
  • ', + '///
      some important stuff
    ', + '///
  • ', + '///
    ',
    +         '///   int some_code = "that should not"',
    +         '///                   "be wrapped"',
    +         '///                   "even' + (' long lines' * 10) + '";',
    +         '/// 
    ']) + self.assertListEqual( + ['/// this is a short paragraph', + '///', + '/// this is a very long line very long line very long line very ' + 'long line', + '/// very long line very long line very long line very long line ' + 'very long', + '/// line very long line', + '///', + '/// more content and some html that should not be wrapped', + '///
  • ', + '///
      some important stuff
    ', + '///
  • ', + '///
    ',
    +         '///   int some_code = "that should not"',
    +         '///                   "be wrapped"',
    +         '///                   "even long lines long lines long lines long '
    +         'lines long lines long lines long lines long lines long lines long '
    +         'lines";',
    +         '/// 
    '], + wrapped_lines) + + def test_paragraph_replacements(self): + """Test applying replacements to a paragraph.""" + parser = generate_constants_lib.DocStringParser( + paragraph_replacements=[('testy test', 'bishy bosh')]) + wrapped_lines = parser.wrap_lines(['/// testy', '/// test']) + self.assertListEqual(['/// bishy bosh'], wrapped_lines) + + def test_get_doc_string_lines(self): + """Test retrival of processed lines.""" + parser = generate_constants_lib.DocStringParser() + parser.parse_line('/// this is a test') + parser.parse_line('/// with two paragraphs') + parser.parse_line('///') + parser.parse_line('/// second paragraph') + self.assertListEqual( + ['/// this is a test with two paragraphs', + '///', + '/// second paragraph'], + parser.get_doc_string_lines()) + + def test_get_doc_string_empty(self): + """Verify an empty string is returned if no documentation is present.""" + parser = generate_constants_lib.DocStringParser() + self.assertEqual('', parser.get_doc_string()) + + def test_get_doc_string(self): + """Verify doc string terminated in a newline is returned.""" + parser = generate_constants_lib.DocStringParser() + parser.parse_line('/// this is a test') + self.assertEqual('/// this is a test\n', parser.get_doc_string()) + + +if __name__ == '__main__': + googletest.main() diff --git a/analytics/src_ios/fake/FIRAnalytics.h b/analytics/src_ios/fake/FIRAnalytics.h new file mode 100644 index 0000000000..060b712c6a --- /dev/null +++ b/analytics/src_ios/fake/FIRAnalytics.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#import + +@interface FIRAnalytics : NSObject + ++ (void)logEventWithName:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters; + ++ (void)setUserPropertyString:(nullable NSString *)value forName:(nonnull NSString *)name; + ++ (void)setUserID:(nullable NSString *)userID; + ++ (void)setScreenName:(nullable NSString *)screenName + screenClass:(nullable NSString *)screenClassOverride; + ++ (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled; + ++ (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval; + ++ (nullable NSString *)appInstanceID; + ++ (void)resetAnalyticsData; + +@end diff --git a/analytics/src_ios/fake/FIRAnalytics.mm b/analytics/src_ios/fake/FIRAnalytics.mm new file mode 100644 index 0000000000..6e2508f276 --- /dev/null +++ b/analytics/src_ios/fake/FIRAnalytics.mm @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#import "analytics/src_ios/fake/FIRAnalytics.h" + +#include "testing/reporter_impl.h" + +@implementation FIRAnalytics + ++ (NSString *)stringForValue:(id)value { + return [NSString stringWithFormat:@"%@", value]; +} + ++ (NSString *)stringForParameters:(NSDictionary *)parameters { + if ([parameters count] == 0) { + return @""; + } + + NSArray *sortedKeys = + [parameters.allKeys sortedArrayUsingSelector:@selector(compare:)]; + NSMutableString *parameterString = [NSMutableString string]; + for (NSString *key in sortedKeys) { + [parameterString appendString:key]; + [parameterString appendString:@"="]; + [parameterString appendString:[self stringForValue:parameters[key]]]; + [parameterString appendString:@","]; + } + // Remove trailing comma from string. + [parameterString deleteCharactersInRange:NSMakeRange([parameterString length] - 1, 1)]; + return parameterString; +} + ++ (void)logEventWithName:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters { + NSString *parameterString = [self stringForParameters:parameters]; + if (parameterString) { + FakeReporter->AddReport("+[FIRAnalytics logEventWithName:parameters:]", + { [name UTF8String], [parameterString UTF8String] }); + } else { + FakeReporter->AddReport("+[FIRAnalytics logEventWithName:parameters:]", + { [name UTF8String] }); + } +} + ++ (void)setUserPropertyString:(nullable NSString *)value forName:(nonnull NSString *)name { + FakeReporter->AddReport("+[FIRAnalytics setUserPropertyString:forName:]", + { [name UTF8String], value ? [value UTF8String] : "nil" }); +} + ++ (void)setUserID:(nullable NSString *)userID { + FakeReporter->AddReport("+[FIRAnalytics setUserID:]", { userID ? [userID UTF8String] : "nil" }); +} + ++ (void)setScreenName:(nullable NSString *)screenName + screenClass:(nullable NSString *)screenClassOverride { + FakeReporter->AddReport("+[FIRAnalytics setScreenName:screenClass:]", + { screenName ? [screenName UTF8String] : "nil", + screenClassOverride ? [screenClassOverride UTF8String] : "nil" }); +} + ++ (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval { + FakeReporter->AddReport( + "+[FIRAnalytics setSessionTimeoutInterval:]", + {[[NSString stringWithFormat:@"%.03f", sessionTimeoutInterval] UTF8String]}); +} + ++ (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled { + FakeReporter->AddReport("+[FIRAnalytics setAnalyticsCollectionEnabled:]", + {analyticsCollectionEnabled ? "YES" : "NO"}); +} + ++ (NSString *)appInstanceID { + FakeReporter->AddReport("+[FIRAnalytics appInstanceID]", {}); + return @"FakeAnalyticsInstanceId0"; +} + ++ (void)resetAnalyticsData { + FakeReporter->AddReport("+[FIRAnalytics resetAnalyticsData]", {}); +} + +@end diff --git a/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java b/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java new file mode 100644 index 0000000000..ef6f0495f8 --- /dev/null +++ b/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.analytics; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.FakeReporter; +import com.google.firebase.testing.cppsdk.TickerAndroid; + +import java.util.TreeSet; + +/** + * Fake for FirebaseAnalytics. + */ +public final class FirebaseAnalytics { + + public static FirebaseAnalytics getInstance(Context context) { + FakeReporter.addReport("FirebaseAnalytics.getInstance"); + return new FirebaseAnalytics(); + } + + public Task getAppInstanceId() { + FakeReporter.addReport("FirebaseAnalytics.getAppInstanceId"); + Task result = Task.forResult("FakeAnalyticsInstanceId0"); + TickerAndroid.register(result); + return result; + } + + public void setAnalyticsCollectionEnabled(boolean enabled) { + FakeReporter.addReport("FirebaseAnalytics.setAnalyticsCollectionEnabled", + Boolean.toString(enabled)); + } + + public void logEvent(String name, Bundle params) { + StringBuilder paramsString = new StringBuilder(); + // Sort keys for predictable ordering. + for (String key : new TreeSet<>(params.keySet())) { + paramsString.append(key); + paramsString.append("="); + paramsString.append(params.get(key)); + paramsString.append(","); + } + paramsString.setLength(Math.max(0, paramsString.length() - 1)); + FakeReporter.addReport("FirebaseAnalytics.logEvent", name, paramsString.toString()); + } + + public void resetAnalyticsData() { + FakeReporter.addReport("FirebaseAnalytics.resetAnalyticsData"); + } + + public void setUserProperty(String name, String value) { + FakeReporter.addReport("FirebaseAnalytics.setUserProperty", name, String.valueOf(value)); + } + + public void setCurrentScreen(Activity activity, String screenName, + String screenClassOverride) { + FakeReporter.addReport("FirebaseAnalytics.setCurrentScreen", activity.getClass().getName(), + String.valueOf(screenName), String.valueOf(screenClassOverride)); + } + + public void setUserId(String userId) { + FakeReporter.addReport("FirebaseAnalytics.setUserId", String.valueOf(userId)); + } + + public void setMinimumSessionDuration(long milliseconds) { + FakeReporter.addReport("FirebaseAnalytics.setMinimumSessionDuration", + Long.toString(milliseconds)); + } + + public void setSessionTimeoutDuration(long milliseconds) { + FakeReporter.addReport("FirebaseAnalytics.setSessionTimeoutDuration", + Long.toString(milliseconds)); + } + +} diff --git a/analytics/tests/CMakeLists.txt b/analytics/tests/CMakeLists.txt new file mode 100644 index 0000000000..51e72f2490 --- /dev/null +++ b/analytics/tests/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright 2019 Google LLC +# +# 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. + + + + +firebase_cpp_cc_test( + firebase_analytics_test + SOURCES + analytics_test.cc + DEPENDS + firebase_app_for_testing + firebase_analytics + firebase_testing +) + +firebase_cpp_cc_test_on_ios( + firebase_analytics_test + HOST + firebase_app_for_testing_ios + SOURCES + ${FIREBASE_SOURCE_DIR}/analytics/tests/analytics_test.cc + DEPENDS + firebase_app_for_testing + firebase_analytics + firebase_testing + "-lsqlite3" + CUSTOM_FRAMEWORKS + StoreKit +) diff --git a/analytics/tests/analytics_test.cc b/analytics/tests/analytics_test.cc new file mode 100644 index 0000000000..3e608cc447 --- /dev/null +++ b/analytics/tests/analytics_test.cc @@ -0,0 +1,310 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include + +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "analytics/src/analytics_common.h" +#include "analytics/src/include/firebase/analytics.h" +#include "app/src/include/firebase/app.h" +#include "app/src/time.h" +#include "app/tests/include/firebase/app_for_testing.h" + +#ifdef __ANDROID__ +#include "app/src/semaphore.h" +#include "app/src/util_android.h" +#endif // __ANDROID__ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace analytics { + +class AnalyticsTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + reporter_.reset(); + + firebase_app_ = testing::CreateApp(); + AddExpectationAndroid("FirebaseAnalytics.getInstance", {}); + analytics::Initialize(*firebase_app_); + } + + void TearDown() override { + firebase::testing::cppsdk::ConfigReset(); + Terminate(); + delete firebase_app_; + firebase_app_ = nullptr; + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + void AddExpectationAndroid(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kAndroid, + args); + } + + void AddExpectationApple(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kIos, args); + } + + // Wait for a task executing on the main thread. + void WaitForMainThreadTask() { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + Semaphore main_thread_signal(0); + util::RunOnMainThread( + firebase_app_->GetJNIEnv(), firebase_app_->activity(), + [](void* data) { reinterpret_cast(data)->Post(); }, + &main_thread_signal); + main_thread_signal.Wait(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + } + + // Wait for a future up to the specified number of milliseconds. + template + static void WaitForFutureWithTimeout(const Future& future, + int timeout_milliseconds, + FutureStatus expected_status) { + while (future.status() != expected_status && timeout_milliseconds-- > 0) { + ::firebase::internal::Sleep(1); + } + } + + App* firebase_app_ = nullptr; + + firebase::testing::cppsdk::Reporter reporter_; +}; + +TEST_F(AnalyticsTest, TestDestroyDefaultApp) { + EXPECT_TRUE(internal::IsInitialized()); + delete firebase_app_; + firebase_app_ = nullptr; + EXPECT_FALSE(internal::IsInitialized()); +} + +TEST_F(AnalyticsTest, TestSetAnalyticsCollectionEnabled) { + AddExpectationAndroid("FirebaseAnalytics.setAnalyticsCollectionEnabled", + {"true"}); + AddExpectationApple("+[FIRAnalytics setAnalyticsCollectionEnabled:]", + {"YES"}); + SetAnalyticsCollectionEnabled(true); +} + +TEST_F(AnalyticsTest, TestSetAnalyticsCollectionDisabled) { + AddExpectationAndroid("FirebaseAnalytics.setAnalyticsCollectionEnabled", + {"false"}); + AddExpectationApple("+[FIRAnalytics setAnalyticsCollectionEnabled:]", {"NO"}); + SetAnalyticsCollectionEnabled(false); +} + +TEST_F(AnalyticsTest, TestLogEventString) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=my_value"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=my_value"}); + + LogEvent("my_event", "my_param", "my_value"); +} + +TEST_F(AnalyticsTest, TestLogEventDouble) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=1.01"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=1.01"}); + LogEvent("my_event", "my_param", 1.01); +} + +TEST_F(AnalyticsTest, TestLogEventInt64) { + int64_t value = 101; + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=101"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=101"}); + + LogEvent("my_event", "my_param", value); +} + +TEST_F(AnalyticsTest, TestLogEventInt) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=101"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=101"}); + + LogEvent("my_event", "my_param", 101); +} + +TEST_F(AnalyticsTest, TestLogEvent) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", {"my_event", ""}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", ""}); + + LogEvent("my_event"); +} + +TEST_F(AnalyticsTest, TestLogEvent40CharName) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"0123456789012345678901234567890123456789", ""}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"0123456789012345678901234567890123456789", ""}); + + LogEvent("0123456789012345678901234567890123456789"); +} + +TEST_F(AnalyticsTest, TestLogEventString40CharName) { + AddExpectationAndroid( + "FirebaseAnalytics.logEvent", + {"my_event", "0123456789012345678901234567890123456789=my_value"}); + AddExpectationApple( + "+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "0123456789012345678901234567890123456789=my_value"}); + LogEvent("my_event", "0123456789012345678901234567890123456789", "my_value"); +} + +TEST_F(AnalyticsTest, TestLogEventString100CharValue) { + const std::string long_string = + "0123456789012345678901234567890123456789" + "012345678901234567890123456789012345678901234567890123456789"; + const std::string result = "my_event=" + long_string; + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", result.c_str()}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", result.c_str()}); + LogEvent("my_event", "my_event", long_string.c_str()); +} + +TEST_F(AnalyticsTest, TestLogEventParameters) { + // Params are sorted alphabetically by mock. + AddExpectationAndroid( + "FirebaseAnalytics.logEvent", + {"my_event", + "my_param_bool=1,my_param_double=1.01,my_param_int=101," + "my_param_string=my_value"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", + "my_param_bool=1,my_param_double=1.01,my_param_int=101," + "my_param_string=my_value"}); + + Parameter parameters[] = { + Parameter("my_param_string", "my_value"), + Parameter("my_param_double", 1.01), + Parameter("my_param_int", 101), + Parameter("my_param_bool", true), + }; + LogEvent("my_event", parameters, sizeof(parameters) / sizeof(parameters[0])); +} + +TEST_F(AnalyticsTest, TestSetUserProperty) { + AddExpectationAndroid("FirebaseAnalytics.setUserProperty", + {"my_property", "my_value"}); + AddExpectationApple("+[FIRAnalytics setUserPropertyString:forName:]", + {"my_property", "my_value"}); + + SetUserProperty("my_property", "my_value"); +} + +TEST_F(AnalyticsTest, TestSetUserPropertyNull) { + AddExpectationAndroid("FirebaseAnalytics.setUserProperty", + {"my_property", "null"}); + AddExpectationApple("+[FIRAnalytics setUserPropertyString:forName:]", + {"my_property", "nil"}); + SetUserProperty("my_property", nullptr); +} + +TEST_F(AnalyticsTest, TestSetUserId) { + AddExpectationAndroid("FirebaseAnalytics.setUserId", {"my_user_id"}); + AddExpectationApple("+[FIRAnalytics setUserID:]", {"my_user_id"}); + SetUserId("my_user_id"); +} + +TEST_F(AnalyticsTest, TestSetUserIdNull) { + AddExpectationAndroid("FirebaseAnalytics.setUserId", {"null"}); + AddExpectationApple("+[FIRAnalytics setUserID:]", {"nil"}); + SetUserId(nullptr); +} + +TEST_F(AnalyticsTest, TestSetSessionTimeoutDuration) { + AddExpectationAndroid("FirebaseAnalytics.setSessionTimeoutDuration", + {"1000"}); + AddExpectationApple("+[FIRAnalytics setSessionTimeoutInterval:]", {"1.000"}); + + SetSessionTimeoutDuration(1000); +} + +TEST_F(AnalyticsTest, TestSetCurrentScreen) { + AddExpectationAndroid("FirebaseAnalytics.setCurrentScreen", + {"android.app.Activity", "my_screen", "my_class"}); + AddExpectationApple("+[FIRAnalytics setScreenName:screenClass:]", + {"my_screen", "my_class"}); + + SetCurrentScreen("my_screen", "my_class"); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestSetCurrentScreenNullScreen) { + AddExpectationAndroid("FirebaseAnalytics.setCurrentScreen", + {"android.app.Activity", "null", "my_class"}); + AddExpectationApple("+[FIRAnalytics setScreenName:screenClass:]", + {"nil", "my_class"}); + + SetCurrentScreen(nullptr, "my_class"); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestSetCurrentScreenNullClass) { + AddExpectationAndroid("FirebaseAnalytics.setCurrentScreen", + {"android.app.Activity", "my_screen", "null"}); + AddExpectationApple("+[FIRAnalytics setScreenName:screenClass:]", + {"my_screen", "nil"}); + + SetCurrentScreen("my_screen", nullptr); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestResetAnalyticsData) { + AddExpectationAndroid("FirebaseAnalytics.resetAnalyticsData", {}); + AddExpectationApple("+[FIRAnalytics resetAnalyticsData]", {}); + AddExpectationApple("+[FIRAnalytics appInstanceID]", {}); + ResetAnalyticsData(); +} + +TEST_F(AnalyticsTest, TestGetAnalyticsInstanceId) { + AddExpectationAndroid("FirebaseAnalytics.getAppInstanceId", {}); + AddExpectationApple("+[FIRAnalytics appInstanceID]", {}); + auto result = GetAnalyticsInstanceId(); + // Wait for up to a second to fetch the ID. + WaitForFutureWithTimeout(result, 1000, firebase::kFutureStatusComplete); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(std::string("FakeAnalyticsInstanceId0"), *result.result()); +} + +} // namespace analytics +} // namespace firebase diff --git a/app/instance_id/instance_id_desktop_impl_test.cc b/app/instance_id/instance_id_desktop_impl_test.cc new file mode 100644 index 0000000000..3c46ddd188 --- /dev/null +++ b/app/instance_id/instance_id_desktop_impl_test.cc @@ -0,0 +1,819 @@ +// Copyright 2019 Google LLC +// +// 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 "app/instance_id/instance_id_desktop_impl.h" + +#include +#include +#include + +#include "app/rest/transport_mock.h" +#include "app/rest/util.h" +#include "app/rest/www_form_url_encoded.h" +#include "app/src/app_identifier.h" +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/future.h" +#include "app/src/include/firebase/version.h" +#include "app/src/log.h" +#include "app/src/secure/user_secure_manager_fake.h" +#include "app/src/time.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "third_party/jsoncpp/testing.h" + +namespace firebase { +namespace instance_id { +namespace internal { +using ::testing::MatchesRegex; +using ::testing::Ne; + +// Access fields and method from another class. Used because only the +// InstanceIdDesktopImplTest class has access, not any of the test case classes. +#define ACCESS_FIELD(object, name, field_type, field_name) \ + static void Set##name(object* impl, field_type value) { \ + impl->field_name = value; \ + } \ + static field_type Get##name(object* impl) { return impl->field_name; } + +#define ACCESS_METHOD0(object, method_return_type, method_name) \ + static method_return_type method_name(object* impl) { \ + return impl->method_name(); \ + } + +#define ACCESS_METHOD1(object, method_return_type, method_name, arg1_type) \ + static method_return_type method_name(object* impl, arg1_type arg1) { \ + return impl->method_name(arg1); \ + } + +#define ACCESS_METHOD2(object, method_return_type, method_name, arg1_type, \ + arg2_type) \ + static method_return_type method_name(object* impl, arg1_type arg1, \ + arg2_type arg2) { \ + return impl->method_name(arg1, arg2); \ + } + +#define SENDER_ID "55662211" +static const char kAppName[] = "app"; +static const char kStorageDomain[] = "iid_test"; +static const char kSampleProjectId[] = "sample_project_id"; +static const char kSamplePackageName[] = "sample.package.name"; +static const char kAppVersion[] = "5.6.7"; +static const char kOsVersion[] = "freedos-1.2.3"; +static const int kPlatform = 100; + +static const char kInstanceId[] = "test_instance_id"; +static const char kDeviceId[] = "test_device_id"; +static const char kSecurityToken[] = "test_security_token"; +static const uint64_t kLastCheckinTimeMs = 0x1234567890L; +static const char kDigest[] = "test_digest"; + +// Mock REST transport that validates request parameters. +class ValidatingTransportMock : public rest::TransportMock { + public: + struct ExpectedRequest { + ExpectedRequest() {} + + ExpectedRequest(const char* body_, bool body_is_json_, + const std::map& headers_) + : body(body_), body_is_json(body_is_json_), headers(headers_) {} + + std::string body; + bool body_is_json; + std::map headers; + }; + + ValidatingTransportMock() {} + + void SetExpectedRequestForUrl(const std::string& url, + const ExpectedRequest& expected) { + expected_request_by_url_[url] = expected; + } + + protected: + void PerformInternal( + rest::Request* request, rest::Response* response, + flatbuffers::unique_ptr* controller_out) override { + std::string body; + EXPECT_TRUE(request->ReadBodyIntoString(&body)); + + auto expected_it = expected_request_by_url_.find(request->options().url); + if (expected_it != expected_request_by_url_.end()) { + const ExpectedRequest& expected = expected_it->second; + if (expected.body_is_json) { + EXPECT_THAT(body, Json::testing::EqualsJson(expected.body)); + } else { + EXPECT_EQ(body, expected.body); + } + EXPECT_EQ(request->options().header, expected.headers); + } + + rest::TransportMock::PerformInternal(request, response, controller_out); + } + + private: + std::map expected_request_by_url_; +}; + +class InstanceIdDesktopImplTest : public ::testing::Test { + protected: + void SetUp() override { + LogSetLevel(kLogLevelDebug); + AppOptions options = testing::MockAppOptions(); + options.set_package_name(kSamplePackageName); + options.set_project_id(kSampleProjectId); + options.set_messaging_sender_id(SENDER_ID); + app_ = testing::CreateApp(options, kAppName); + impl_ = InstanceIdDesktopImpl::GetInstance(app_); + SetUserSecureManager( + impl_, + MakeUnique( + kStorageDomain, + firebase::internal::CreateAppIdentifierFromOptions(app_->options()) + .c_str())); + transport_ = new ValidatingTransportMock(); + SetTransport(impl_, UniquePtr(transport_)); + } + + void TearDown() override { + DeleteFromStorage(impl_); + delete impl_; + delete app_; + transport_ = nullptr; + } + + // Busy waits until |future| has completed. + void WaitForFuture(const FutureBase& future) { + ASSERT_THAT(future.status(), Ne(FutureStatus::kFutureStatusInvalid)); + while (true) { + if (future.status() != FutureStatus::kFutureStatusPending) { + break; + } + } + } + + // Create accessors / mutators for private fields in InstanceIdDesktopImpl. + ACCESS_FIELD(InstanceIdDesktopImpl, UserSecureManager, + UniquePtr, + user_secure_manager_); + ACCESS_FIELD(InstanceIdDesktopImpl, InstanceId, std::string, instance_id_); + typedef std::map TokenMap; + ACCESS_FIELD(InstanceIdDesktopImpl, Tokens, TokenMap, tokens_); + ACCESS_FIELD(InstanceIdDesktopImpl, Locale, std::string, locale_); + ACCESS_FIELD(InstanceIdDesktopImpl, Timezone, std::string, timezone_); + ACCESS_FIELD(InstanceIdDesktopImpl, LoggingId, int, logging_id_); + ACCESS_FIELD(InstanceIdDesktopImpl, IosDeviceModel, std::string, + ios_device_model_); + ACCESS_FIELD(InstanceIdDesktopImpl, IosDeviceVersion, std::string, + ios_device_version_); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataLastCheckinTimeMs, uint64_t, + checkin_data_.last_checkin_time_ms); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataSecurityToken, std::string, + checkin_data_.security_token); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataDeviceId, std::string, + checkin_data_.device_id); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataDigest, std::string, + checkin_data_.digest); + ACCESS_FIELD(InstanceIdDesktopImpl, AppVersion, std::string, app_version_); + ACCESS_FIELD(InstanceIdDesktopImpl, OsVersion, std::string, os_version_); + ACCESS_FIELD(InstanceIdDesktopImpl, Platform, int, platform_); + ACCESS_FIELD(InstanceIdDesktopImpl, Transport, UniquePtr, + transport_); + // Create wrappers for private methods in InstanceIdDesktopImpl. + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, SaveToStorage); + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, LoadFromStorage); + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, DeleteFromStorage); + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, InitialOrRefreshCheckin); + ACCESS_METHOD0(InstanceIdDesktopImpl, std::string, GenerateAppId); + ACCESS_METHOD2(InstanceIdDesktopImpl, bool, FetchServerToken, const char*, + bool*); + ACCESS_METHOD2(InstanceIdDesktopImpl, bool, DeleteServerToken, const char*, + bool); + + InstanceIdDesktopImpl* impl_; + App* app_; + ValidatingTransportMock* transport_; +}; + +TEST_F(InstanceIdDesktopImplTest, TestInitialization) { + // Does everything initialize and delete properly? Checked automatically. +} + +TEST_F(InstanceIdDesktopImplTest, TestSaveAndLoad) { + SetInstanceId(impl_, kInstanceId); + SetCheckinDataLastCheckinTimeMs(impl_, kLastCheckinTimeMs); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataDigest(impl_, kDigest); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + + // Save to storage. + EXPECT_TRUE(SaveToStorage(impl_)); + + // Zero out the in-memory version so we need to load from storage. + SetInstanceId(impl_, ""); + SetCheckinDataLastCheckinTimeMs(impl_, 0); + SetCheckinDataDeviceId(impl_, ""); + SetCheckinDataSecurityToken(impl_, ""); + SetCheckinDataDigest(impl_, ""); + SetTokens(impl_, std::map()); + + // Make sure the data is zeroed out. + EXPECT_EQ("", GetInstanceId(impl_)); + EXPECT_EQ(0, GetCheckinDataLastCheckinTimeMs(impl_)); + EXPECT_EQ("", GetCheckinDataDeviceId(impl_)); + EXPECT_EQ("", GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ("", GetCheckinDataDigest(impl_)); + EXPECT_EQ(0, GetTokens(impl_).size()); + + // Load the data from storage. + EXPECT_TRUE(LoadFromStorage(impl_)); + + // Ensure that the loaded data is correct. + EXPECT_EQ(kInstanceId, GetInstanceId(impl_)); + EXPECT_EQ(kLastCheckinTimeMs, GetCheckinDataLastCheckinTimeMs(impl_)); + EXPECT_EQ(kDeviceId, GetCheckinDataDeviceId(impl_)); + EXPECT_EQ(kSecurityToken, GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ(kDigest, GetCheckinDataDigest(impl_)); + EXPECT_EQ(tokens, GetTokens(impl_)); + + EXPECT_TRUE(DeleteFromStorage(impl_)); + EXPECT_FALSE(LoadFromStorage(impl_)) + << "LoadFromStorage() should return false after deletion."; +} + +TEST_F(InstanceIdDesktopImplTest, TestGenerateAppId) { + const int kNumAppIds = 100; // Generate 100 AppIDs. + std::set generated_app_ids; + + for (int i = 0; i < kNumAppIds; ++i) { + std::string app_id = GenerateAppId(impl_); + + // AppIDs are always 11 bytes long. + EXPECT_EQ(app_id.length(), 11) << "Bad length: " << app_id; + + // AppIDs always start with c, d, e, or f, since the first 4 bits are 0x7. + EXPECT_TRUE(app_id[0] == 'c' || app_id[0] == 'd' || app_id[0] == 'e' || + app_id[0] == 'f') + << "Invalid first character: " << app_id; + + // AppIDs should only consist of [A-Za-z0-9_-] + EXPECT_THAT(app_id, MatchesRegex("^[A-Za-z0-9_-]*$")); + + // The same AppIDs should never be generated twice, so ensure no collision + // occurred. In theory this may be slightly flaky, but in practice if it + // actually collides with only 100 AppIDs, then we have a bigger problem. + EXPECT_TRUE(generated_app_ids.find(app_id) == generated_app_ids.end()) + << "Got an AppID collision: " << app_id; + generated_app_ids.insert(app_id); + } +} + +TEST_F(InstanceIdDesktopImplTest, CheckinFailure) { + // Backend returns an error. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 405 Method Not Allowed']," + " body: ['']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(InitialOrRefreshCheckin(impl_)); + + // Backend returns a malformed response. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['a bad response']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(InitialOrRefreshCheckin(impl_)); +} + +#define CHECKIN_SECURITY_TOKEN "123456789" +#define CHECKIN_DEVICE_ID "987654321" +#define CHECKIN_DIGEST "CA/fDTryF5eVxjNF8ZIJAg==" + +#define CHECKIN_RESPONSE_BODY \ + " {" \ + " \"device_data_version_info\":" \ + "\"DEVICE_DATA_VERSION_INFO_PLACEHOLDER\"," \ + " \"stats_ok\":\"1\"," \ + " \"security_token\":" CHECKIN_SECURITY_TOKEN \ + "," \ + " \"digest\":\"" CHECKIN_DIGEST \ + "\"," \ + " \"time_msec\":1557948713568," \ + " \"version_info\":\"0-qhPDIT2HYXIJ42qPW9kfDzoKzPqxY\"," \ + " \"android_id\":" CHECKIN_DEVICE_ID \ + "," \ + " \"intent\":[" \ + " {\"action\":\"com.google.android.gms.checkin.NOOP\"}" \ + " ]," \ + " \"setting\":[" \ + " {\"name\":\"android_id\"," \ + " \"value\":\"" CHECKIN_DEVICE_ID \ + "\"}," \ + " {\"name\":\"device_country\"," \ + " \"value\":\"us\"}," \ + " {\"name\":\"device_registration_time\"," \ + " \"value\":\"1557946800000\"}," \ + " {\"name\":\"ios_device\"," \ + " \"value\":\"1\"}" \ + " ]" \ + " }" + +TEST_F(InstanceIdDesktopImplTest, Checkin) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }" + " ]" + "}"); + +#define CHECKIN_TIMEZONE "America/Los_Angeles" +#define CHECKIN_LOCALE "en_US" +#define CHECKIN_IOS_DEVICE_MODEL "iPhone 8" +#define CHECKIN_IOS_DEVICE_VERSION "8.0" +#define CHECKIN_LOGGING_ID 11223344 + + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationJson; + headers[rest::util::kContentType] = rest::util::kApplicationJson; + transport_->SetExpectedRequestForUrl( + "https://device-provisioning.googleapis.com/checkin", + ValidatingTransportMock::ExpectedRequest( + "{ \"checkin\": " + "{ \"iosbuild\": " + "{ \"model\": \"" CHECKIN_IOS_DEVICE_MODEL "\", " + "\"os_version\": \"" CHECKIN_IOS_DEVICE_VERSION "\" }, " + "\"last_checkin_msec\": 0, \"type\": 2, \"user_number\": 0 }, " + "\"digest\": \"\", \"fragment\": 0, \"id\": 0, " + "\"locale\": \"en_US\", " + "\"logging_id\": " FIREBASE_STRING( + CHECKIN_LOGGING_ID) ", " + "\"security_token\": 0, \"timezone\": " + "\"" CHECKIN_TIMEZONE "\", " + "\"user_serial_number\": 0, \"version\": 2 }", + true, headers)); + SetLocale(impl_, CHECKIN_LOCALE); + SetTimezone(impl_, CHECKIN_TIMEZONE); + SetLoggingId(impl_, CHECKIN_LOGGING_ID); + SetIosDeviceModel(impl_, CHECKIN_IOS_DEVICE_MODEL); + SetIosDeviceVersion(impl_, CHECKIN_IOS_DEVICE_VERSION); + EXPECT_TRUE(InitialOrRefreshCheckin(impl_)); + + // Make sure the logged checkin time is within a second. + EXPECT_LT(firebase::internal::GetTimestamp() - + GetCheckinDataLastCheckinTimeMs(impl_), + 1000); + // Check the cached check-in data. + EXPECT_EQ(CHECKIN_SECURITY_TOKEN, GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ(CHECKIN_DEVICE_ID, GetCheckinDataDeviceId(impl_)); + EXPECT_EQ(CHECKIN_DIGEST, GetCheckinDataDigest(impl_)); + + // Try checking in again, this should do nothing as the credentials haven't + // expired. + firebase::testing::cppsdk::ConfigSet("{}"); + transport_->SetExpectedRequestForUrl( + "https://device-provisioning.googleapis.com/checkin", + ValidatingTransportMock::ExpectedRequest()); + EXPECT_TRUE(InitialOrRefreshCheckin(impl_)); + // Make sure the cached check-in data didn't change. + EXPECT_EQ(CHECKIN_SECURITY_TOKEN, GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ(CHECKIN_DEVICE_ID, GetCheckinDataDeviceId(impl_)); + EXPECT_EQ(CHECKIN_DIGEST, GetCheckinDataDigest(impl_)); + +#undef CHECKIN_TIMEZONE +#undef CHECKIN_LOCALE +#undef CHECKIN_IOS_DEVICE_MODEL +#undef CHECKIN_IOS_DEVICE_VERSION +#undef CHECKIN_LOGGING_ID +#undef CHECKIN_LOGGING_ID_STRING +} + +#define FETCH_TOKEN "atoken" + +TEST_F(InstanceIdDesktopImplTest, FetchServerToken) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }," + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['token=" FETCH_TOKEN + "']" + " }" + " }" + " ]" + "}"); + + // Set token fetch parameters. + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + // TODO(smiles): The following two lines should be removed when we're + // generating the IID. + SetInstanceId(impl_, kInstanceId); + SaveToStorage(impl_); + std::string expected_request; + { + rest::WwwFormUrlEncoded form(&expected_request); + form.Add("sender", SENDER_ID); + form.Add("app", kSamplePackageName); + form.Add("app_ver", kAppVersion); + form.Add("device", CHECKIN_DEVICE_ID); + form.Add("X-scope", "*"); + form.Add("X-subtype", SENDER_ID); + form.Add("X-osv", kOsVersion); + form.Add("plat", "100"); + form.Add("app_id", kInstanceId); + } + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kContentType] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kAuthorization] = + std::string("AidLogin ") + std::string(CHECKIN_DEVICE_ID) + + std::string(":") + std::string(CHECKIN_SECURITY_TOKEN); + transport_->SetExpectedRequestForUrl( + "https://fcmtoken.googleapis.com/register", + ValidatingTransportMock::ExpectedRequest(expected_request.c_str(), false, + headers)); + bool retry; + EXPECT_TRUE(FetchServerToken(impl_, "*", &retry)); + EXPECT_FALSE(retry); + + std::map expected_tokens; + expected_tokens["*"] = FETCH_TOKEN; + EXPECT_EQ(expected_tokens, GetTokens(impl_)); +} + +TEST_F(InstanceIdDesktopImplTest, FetchServerTokenRegistrationError) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }," + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['Error=PHONE_REGISTRATION_ERROR&token=" FETCH_TOKEN + "sender=55662211" + "']" + " }" + " }" + " ]" + "}"); + + // Set token fetch parameters. + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + // TODO(smiles): The following two lines should be removed when we're + // generating the IID. + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = FETCH_TOKEN; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + bool retry; + EXPECT_FALSE(FetchServerToken(impl_, "fcm", &retry)); + EXPECT_TRUE(retry); + EXPECT_EQ(1, GetTokens(impl_).size()); +} + +TEST_F(InstanceIdDesktopImplTest, FetchServerTokenExpired) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }," + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['Error=foo%3Abar%3Aother%20stuff%3ARST&token=" FETCH_TOKEN + "sender=55662211" + "']" + " }" + " }" + " ]" + "}"); + + // Set token fetch parameters. + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + // TODO(smiles): The following two lines should be removed when we're + // generating the IID. + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = FETCH_TOKEN; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + bool retry; + EXPECT_FALSE(FetchServerToken(impl_, "fcm", &retry)); + EXPECT_FALSE(retry); + EXPECT_EQ(0, GetTokens(impl_).size()); +} + +#undef FETCH_TOKEN +#undef CHECKIN_SECURITY_TOKEN +#undef CHECKIN_DEVICE_ID +#undef CHECKIN_DIGEST +#undef CHECKIN_RESPONSE_BODY + +TEST_F(InstanceIdDesktopImplTest, FetchServerTokenFailure) { + // Backend returns an error. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 405 Method Not Allowed']," + " body: ['']" + " }" + " }" + " ]" + "}"); + bool retry; + EXPECT_FALSE(FetchServerToken(impl_, "*", &retry)); + EXPECT_FALSE(retry); + EXPECT_EQ(0, GetTokens(impl_).size()); + + // Backend returns an invalid response. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['foo=bar&wibble=wobble']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(FetchServerToken(impl_, "*", &retry)); + EXPECT_FALSE(retry); + EXPECT_EQ(0, GetTokens(impl_).size()); +} + +TEST_F(InstanceIdDesktopImplTest, DeleteServerTokenNoop) { + // Deleting a token that doesn't exist should succeed. + EXPECT_TRUE(DeleteServerToken(impl_, nullptr, true)); + EXPECT_TRUE(DeleteServerToken(impl_, "fcm", false)); +} + +TEST_F(InstanceIdDesktopImplTest, DeleteServerToken) { + const char* kResponses[] = { + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['token=" SENDER_ID + "']" + " }" + " }" + " ]" + "}", + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['deleted=" SENDER_ID + "']" + " }" + " }" + " ]" + "}", + }; + for (size_t i = 0; i < sizeof(kResponses) / sizeof(kResponses[0]); ++i) { + firebase::testing::cppsdk::ConfigSet(kResponses[i]); + + std::string expected_request; + { + rest::WwwFormUrlEncoded form(&expected_request); + form.Add("sender", SENDER_ID); + form.Add("app", kSamplePackageName); + form.Add("app_ver", kAppVersion); + form.Add("device", kDeviceId); + form.Add("X-scope", "fcm"); + form.Add("X-subtype", SENDER_ID); + form.Add("X-osv", kOsVersion); + form.Add("plat", "100"); + form.Add("app_id", kInstanceId); + form.Add("delete", "true"); + } + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kContentType] = + rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kAuthorization] = + std::string("AidLogin ") + std::string(kDeviceId) + std::string(":") + + std::string(kSecurityToken); + + transport_->SetExpectedRequestForUrl( + "https://fcmtoken.googleapis.com/register", + ValidatingTransportMock::ExpectedRequest(expected_request.c_str(), + false, headers)); + + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataLastCheckinTimeMs(impl_, firebase::internal::GetTimestamp()); + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + EXPECT_TRUE(DeleteServerToken(impl_, "fcm", false)) << "Iteration " << i; + + std::map expected_tokens; + expected_tokens["*"] = "123456789"; + EXPECT_EQ(expected_tokens, GetTokens(impl_)); + + // Clean up storage before the next iteration. + DeleteFromStorage(impl_); + } +} + +TEST_F(InstanceIdDesktopImplTest, DeleteTokenFailed) { + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataLastCheckinTimeMs(impl_, firebase::internal::GetTimestamp()); + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + // Delete a token that isn't present. + EXPECT_TRUE(DeleteServerToken(impl_, "non-existent-token", false)); + EXPECT_EQ(tokens, GetTokens(impl_)); + + // Delete a token with a server failure. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 405 Method Not Allowed']," + " body: ['']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(DeleteServerToken(impl_, "fcm", false)); + EXPECT_EQ(tokens, GetTokens(impl_)); + + // Delete a token with an invalid server response. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['everything is just fine']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(DeleteServerToken(impl_, "fcm", false)); + EXPECT_EQ(tokens, GetTokens(impl_)); +} + +TEST_F(InstanceIdDesktopImplTest, DeleteAllServerTokens) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['token=" SENDER_ID + "']" + " }" + " }" + " ]" + "}"); + + std::string expected_request; + { + rest::WwwFormUrlEncoded form(&expected_request); + form.Add("sender", SENDER_ID); + form.Add("app", kSamplePackageName); + form.Add("app_ver", kAppVersion); + form.Add("device", kDeviceId); + form.Add("X-scope", "*"); + form.Add("X-subtype", SENDER_ID); + form.Add("X-osv", kOsVersion); + form.Add("plat", "100"); + form.Add("app_id", kInstanceId); + form.Add("delete", "true"); + form.Add("iid-operation", "delete"); + } + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kContentType] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kAuthorization] = + std::string("AidLogin ") + std::string(kDeviceId) + std::string(":") + + std::string(kSecurityToken); + + transport_->SetExpectedRequestForUrl( + "https://fcmtoken.googleapis.com/register", + ValidatingTransportMock::ExpectedRequest(expected_request.c_str(), false, + headers)); + + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataLastCheckinTimeMs(impl_, firebase::internal::GetTimestamp()); + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + EXPECT_TRUE(DeleteServerToken(impl_, nullptr, true)); + EXPECT_EQ(0, GetTokens(impl_).size()); +} + +} // namespace internal +} // namespace instance_id +} // namespace firebase diff --git a/app/memory/atomic_test.cc b/app/memory/atomic_test.cc new file mode 100644 index 0000000000..8ecea4e9b9 --- /dev/null +++ b/app/memory/atomic_test.cc @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/memory/atomic.h" + +#include // NOLINT +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +// Basic sanity tests for atomic operations. + +namespace firebase { +namespace compat { +namespace { + +using ::testing::Eq; + +const uint64_t kValue = 10; +const uint64_t kUpdatedValue = 20; + +TEST(AtomicTest, DefaultConstructedAtomicIsEqualToZero) { + Atomic atomic; + EXPECT_THAT(atomic.load(), Eq(0)); +} + +TEST(AtomicTest, AssignedValueIsProperlyLoadedViaLoad) { + Atomic atomic(kValue); + EXPECT_THAT(atomic.load(), Eq(kValue)); +} + +TEST(AtomicTest, FetchAddProperlyAddsValueAndReturnsValueBeforeAddition) { + Atomic atomic(kValue); + EXPECT_THAT(atomic.fetch_add(kValue), kValue); + EXPECT_THAT(atomic.load(), Eq(2 * kValue)); +} + +TEST(AtomicTest, + FetchSubProperlySubtractsValueAndReturnsValueBeforeSubtraction) { + Atomic atomic(kValue); + EXPECT_THAT(atomic.fetch_sub(kValue), kValue); + EXPECT_THAT(atomic.load(), Eq(0)); +} + +TEST(AtomicTest, NewValueIsProperlyAssignedWithAssignmentOperator) { + Atomic atomic; + atomic = kValue; + EXPECT_THAT(atomic.load(), Eq(kValue)); +} + +// Note: This test needs to spin and can't use synchronization like +// mutex+condvar because their use renders the test useless due to the fact that +// in the presence of synchronization non-atomic updates are also guaranteed to +// be visible across threads. +TEST(AtomicTest, AtomicUpdatesAreVisibleAcrossThreads) { + Atomic atomic(kValue); + + std::thread thread([&atomic]() { + while (atomic.load() == kValue) { + } + atomic.fetch_add(1); + }); + atomic.store(kUpdatedValue); + thread.join(); + + EXPECT_THAT(atomic.load(), Eq(kUpdatedValue + 1)); +} + +TEST(AtomicTest, AtomicUpdatesAreVisibleAcrossMultipleThreads) { + Atomic atomic; + + const int num_threads = 10; + std::vector threads; + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back([&atomic] { atomic.fetch_add(1); }); + } + for (auto& thread : threads) { + thread.join(); + } + EXPECT_THAT(atomic.load(), Eq(num_threads)); +} + +} // namespace +} // namespace compat +} // namespace firebase diff --git a/app/memory/shared_ptr_test.cc b/app/memory/shared_ptr_test.cc new file mode 100644 index 0000000000..7f76b816b0 --- /dev/null +++ b/app/memory/shared_ptr_test.cc @@ -0,0 +1,229 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/memory/shared_ptr.h" + +#include // NOLINT +#include + +#include "app/memory/atomic.h" +#include "app/meta/move.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::firebase::compat::Atomic; +using ::testing::Eq; +using ::testing::IsNull; + +class Destructable { + public: + explicit Destructable(Atomic* destroyed) : destroyed_(destroyed) {} + virtual ~Destructable() { destroyed_->fetch_add(1); } + + private: + Atomic* const destroyed_; +}; + +class Derived : public Destructable { + public: + explicit Derived(Atomic* destroyed) : Destructable(destroyed) {} +}; + +TEST(SharedPtrTest, DefaultConstructedSharedPtrDoesNotManageAnObject) { + SharedPtr ptr; + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); +} + +TEST(SharedPtrTest, EmptySharedPtrCopiesDoNotManageAnObject) { + SharedPtr ptr; + SharedPtr ptr2(ptr); // NOLINT + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr2.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); + EXPECT_THAT(ptr2.get(), Eq(nullptr)); +} + +TEST(SharedPtrTest, NullptrConstructedSharedPtrDoesNotManageAnObject) { + SharedPtr ptr(static_cast(nullptr)); + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); +} + +TEST(SharedPtrTest, WrapSharedCreatesValidSharedPtr) { + Atomic destroyed; + { + auto destructable = new Destructable(&destroyed); + auto ptr = WrapShared(destructable); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, SharedPtrCorrectlyDestroysTheContainedObject) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, CopiesShareTheSameObjectWhichIsDestroyedOnlyOnce) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + auto ptr2 = ptr; // NOLINT + EXPECT_THAT(ptr.use_count(), Eq(2)); + EXPECT_THAT(ptr.get(), Eq(ptr2.get())); + } + EXPECT_THAT(ptr.use_count(), Eq(1)); + EXPECT_THAT(destroyed.load(), Eq(0)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, MoveCorrectlyTransfersOwnership) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + auto* managed = ptr.get(); + auto ptr2 = Move(ptr); + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); + EXPECT_THAT(ptr2.use_count(), Eq(1)); + EXPECT_THAT(ptr2.get(), Eq(managed)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, + ConvertingCopiesShareTheSameObjectWhichIsDestroyedOnlyOnce) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + SharedPtr ptr2(ptr); // NOLINT + EXPECT_THAT(ptr.use_count(), Eq(2)); + EXPECT_THAT(ptr.get(), Eq(ptr2.get())); + } + EXPECT_THAT(ptr.use_count(), Eq(1)); + EXPECT_THAT(destroyed.load(), Eq(0)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, ConvertingMoveCorrectlyTransfersOwnership) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + auto* managed = ptr.get(); + SharedPtr ptr2(Move(ptr)); + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); + EXPECT_THAT(ptr2.use_count(), Eq(1)); + EXPECT_THAT(ptr2.get(), Eq(managed)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, EmptySharedPtrIsFalseWhenConvertedToBool) { + SharedPtr ptr; + EXPECT_FALSE(ptr); +} + +TEST(SharedPtrTest, NontEmptySharedPtrIsTrueWhenConvertedToBool) { + auto ptr = MakeShared(1); + EXPECT_TRUE(ptr); +} + +TEST(SharedPtrTest, + SharedPtrRefCountIsThreadSafeAndOnlyDeletesTheManagedPtrOnce) { + Atomic destroyed; + std::vector threads; + { + auto ptr = MakeShared(&destroyed); + + for (int i = 0; i < 10; ++i) { + threads.emplace_back([ptr] { + // make another copy. + auto ptr2 = ptr; // NOLINT + }); + } + EXPECT_THAT(destroyed.load(), Eq(0)); + } + for (auto& thread : threads) { + thread.join(); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, CopySharedPtr) { + SharedPtr *value1 = new SharedPtr(new int(10)); + SharedPtr *value2 = new SharedPtr(); + *value2 = *value1; + delete value1; + EXPECT_THAT(**value2, 10); + delete value2; +} + +TEST(SharedPtrTest, CopySharedPtrDereferenceTest) { + SharedPtr ptr1 = MakeShared(10); + SharedPtr ptr2 = MakeShared(10); + SharedPtr ptr3 = MakeShared(10); + SharedPtr ptr = ptr1; + ptr = ptr2; + EXPECT_THAT(ptr1.use_count(), Eq(1)); + ptr = ptr3; + EXPECT_THAT(ptr2.use_count(), Eq(1)); +} + +TEST(SharedPtrTest, SharedPtrReset) { + SharedPtr ptr1 = MakeShared(10); + ptr1.reset(); + EXPECT_THAT(ptr1.get(), IsNull()); // NOLINT + + SharedPtr ptr2 = MakeShared(10); + SharedPtr ptr3 = ptr2; + ptr3.reset(); + EXPECT_THAT(ptr3.get(), IsNull()); // NOLINT + EXPECT_THAT(ptr2.use_count(), Eq(1)); +} + +TEST(SharedPtrTest, MoveSharedPtr) { + SharedPtr value1(new int(10)); + SharedPtr value2; + EXPECT_THAT(*value1, Eq(10)); + value2 = Move(value1); + EXPECT_THAT(value1.get(), IsNull()); // NOLINT + EXPECT_THAT(*value2, Eq(10)); +} + +} // namespace +} // namespace firebase diff --git a/app/memory/unique_ptr_test.cc b/app/memory/unique_ptr_test.cc new file mode 100644 index 0000000000..541a89d9e6 --- /dev/null +++ b/app/memory/unique_ptr_test.cc @@ -0,0 +1,174 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/memory/unique_ptr.h" + +#include "app/meta/move.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::testing::Eq; + +typedef void (*OnDestroyFn)(bool*); + +class Destructable { + public: + explicit Destructable(bool* destroyed) : destroyed_(destroyed) {} + ~Destructable() { *destroyed_ = true; } + + bool destroyed() const { return destroyed_; } + + private: + bool* const destroyed_; +}; + +class Base { + public: + virtual ~Base() {} +}; + +class Derived : public Base { + public: + Derived(bool* destroyed) : destroyed_(destroyed) {} + ~Derived() override { *destroyed_ = true; } + bool destroyed() const { return destroyed_; } + + private: + bool* const destroyed_; +}; + +void Foo(UniquePtr b) {} + +void AssertRawPtrEq(const UniquePtr& ptr, Destructable* value) { + ASSERT_THAT(ptr.get(), Eq(value)); + ASSERT_THAT(ptr.operator->(), Eq(value)); + + if (value != nullptr) { + ASSERT_THAT((*ptr).destroyed(), Eq(value->destroyed())); + ASSERT_THAT(ptr->destroyed(), Eq(value->destroyed())); + } +} + +TEST(UniquePtrTest, DeletesContainingPtrWhenDestroyed) { + bool destroyed = false; + { MakeUnique(&destroyed); } + EXPECT_THAT(destroyed, Eq(true)); +} + +TEST(UniquePtrTest, DoesNotDeleteContainingPtrWhenDestroyedIfReleased) { + bool destroyed = false; + Destructable* raw_ptr; + { + auto ptr = MakeUnique(&destroyed); + raw_ptr = ptr.release(); + } + EXPECT_THAT(destroyed, Eq(false)); + delete raw_ptr; + EXPECT_THAT(destroyed, Eq(true)); +} + +TEST(UniquePtrTest, MoveConstructionTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed = false; + { + auto ptr = MakeUnique(&destroyed); + auto* raw_ptr = ptr.get(); + auto movedInto = UniquePtr(Move(ptr)); + + AssertRawPtrEq(ptr, nullptr); + AssertRawPtrEq(movedInto, raw_ptr); + } +} + +TEST(UniquePtrTest, CopyConstructionTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed = false; + { + auto ptr = MakeUnique(&destroyed); + auto* raw_ptr = ptr.get(); + auto movedInto = UniquePtr(ptr); + + AssertRawPtrEq(ptr, nullptr); + AssertRawPtrEq(movedInto, raw_ptr); + } +} + +TEST(UniquePtrTest, MoveAssignmentTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed1 = false; + bool destroyed2 = false; + { + auto ptr1 = MakeUnique(&destroyed1); + auto ptr2 = MakeUnique(&destroyed2); + + auto* raw_ptr2 = ptr2.get(); + ptr1 = Move(ptr2); + + ASSERT_THAT(destroyed1, Eq(true)); + AssertRawPtrEq(ptr1, raw_ptr2); + AssertRawPtrEq(ptr2, nullptr); + } + ASSERT_THAT(destroyed2, Eq(true)); +} + +TEST(UniquePtrTest, CopyAssignmentTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed1 = false; + bool destroyed2 = false; + { + auto ptr1 = MakeUnique(&destroyed1); + auto ptr2 = MakeUnique(&destroyed2); + + auto* raw_ptr2 = ptr2.get(); + ptr1 = ptr2; + + ASSERT_THAT(destroyed1, Eq(true)); + AssertRawPtrEq(ptr1, raw_ptr2); + AssertRawPtrEq(ptr2, nullptr); + } + ASSERT_THAT(destroyed2, Eq(true)); +} + +TEST(UniquePtrTest, MoveAssignmentToEmptyTransfersOwnershipOfThePtr) { + bool destroyed = false; + { + UniquePtr ptr; + AssertRawPtrEq(ptr, nullptr); + + auto raw_ptr = new Destructable(&destroyed); + ptr = raw_ptr; + AssertRawPtrEq(ptr, raw_ptr); + } + ASSERT_THAT(destroyed, Eq(true)); +} + +TEST(UniquePtrTest, EmptyUniquePtrImplicitlyConvertsToFalse) { + UniquePtr ptr; + EXPECT_THAT(ptr, Eq(false)); +} + +TEST(UniquePtrTest, NonEmptyUniquePtrImplicitlyConvertsToTrue) { + auto ptr = MakeUnique(10); + EXPECT_THAT(ptr, Eq(true)); +} + +TEST(UniquePtrTest, UniquePtrToDerivedConvertsToBase) { + bool destroyed = false; + { UniquePtr base_ptr = MakeUnique(&destroyed); } + EXPECT_THAT(destroyed, Eq(true)); +} + +} // namespace +} // namespace firebase diff --git a/app/meta/move_test.cc b/app/meta/move_test.cc new file mode 100644 index 0000000000..3b947f2a04 --- /dev/null +++ b/app/meta/move_test.cc @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/meta/move.h" + +#include "app/src/include/firebase/internal/type_traits.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::testing::Eq; + +class MoveTester { + public: + MoveTester() = default; + MoveTester(const MoveTester&) = default; + MoveTester(MoveTester&& other) : moved_(true) {} + MoveTester& operator=(MoveTester&& other) { + moved_ = true; + return *this; + } + bool moved() const { return moved_; } + + private: + bool moved_ = false; +}; + +TEST(MoveTest, DefaultConstructedMoveTesterIsNotMoved) { + MoveTester tester; + ASSERT_THAT(tester.moved(), Eq(false)); +} + +TEST(MoveTest, CopyConstructedMoveTesterIsNotMoved) { + MoveTester tester; + MoveTester copiedTester(tester); + ASSERT_THAT(copiedTester.moved(), Eq(false)); +} + +TEST(MoveTest, MoveConstructedMoveTesterIsMoved) { + MoveTester tester; + MoveTester copiedTester(Move(tester)); + ASSERT_THAT(copiedTester.moved(), Eq(true)); +} + +TEST(MoveTest, MoveAssignedMoveTesterIsMoved) { + MoveTester tester1; + MoveTester tester2; + tester2 = Move(tester1); + ASSERT_THAT(tester2.moved(), Eq(true)); +} + +} // namespace +} // namespace firebase diff --git a/app/rest/tests/gzipheader_unittest.cc b/app/rest/tests/gzipheader_unittest.cc new file mode 100644 index 0000000000..b95375462a --- /dev/null +++ b/app/rest/tests/gzipheader_unittest.cc @@ -0,0 +1,162 @@ +// +// Copyright 2003 Google LLC All rights reserved. +// +// 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. +// +// Author: Neal Cardwell + +#include "app/rest/gzipheader.h" + +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "absl/base/macros.h" +#include "absl/strings/escaping.h" +#include "util/random/acmrandom.h" + +namespace firebase { + +// Take some test headers and pass them to a GZipHeader, fragmenting +// the headers in many different random ways. +TEST(GzipHeader, FragmentTest) { + ACMRandom rnd(ACMRandom::DeprecatedDefaultSeed()); + + struct TestCase { + const char* str; + int len; // total length of the string + int cruft_len; // length of the gzip header part + }; + TestCase tests[] = { + // Basic header: + {"\037\213\010\000\216\176\356\075\002\003", 10, 0}, + + // Basic headers with crud on the end: + {"\037\213\010\000\216\176\356\075\002\003X", 11, 1}, + {"\037\213\010\000\216\176\356\075\002\003XXX", 13, 3}, + + { + "\037\213\010\010\321\135\265\100\000\003" + "emacs\000", + 16, 0 // with an FNAME of "emacs" + }, + { + "\037\213\010\010\321\135\265\100\000\003" + "\000", + 11, 0 // with an FNAME of zero bytes + }, + { + "\037\213\010\020\321\135\265\100\000\003" + "emacs\000", + 16, 0, // with an FCOMMENT of "emacs" + }, + { + "\037\213\010\020\321\135\265\100\000\003" + "\000", + 11, 0, // with an FCOMMENT of zero bytes + }, + { + "\037\213\010\002\321\135\265\100\000\003" + "\001\002", + 12, 0 // with an FHCRC + }, + { + "\037\213\010\004\321\135\265\100\000\003" + "\003\000foo", + 15, 0 // with an extra of "foo" + }, + { + "\037\213\010\004\321\135\265\100\000\003" + "\000\000", + 12, 0 // with an extra of zero bytes + }, + { + "\037\213\010\032\321\135\265\100\000\003" + "emacs\000" + "emacs\000" + "\001\002", + 24, 0 // with an FNAME of "emacs", FCOMMENT of "emacs", and FHCRC + }, + { + "\037\213\010\036\321\135\265\100\000\003" + "\003\000foo" + "emacs\000" + "emacs\000" + "\001\002", + 29, 0 // with an FNAME of "emacs", FCOMMENT of "emacs", FHCRC, "foo" + }, + { + "\037\213\010\036\321\135\265\100\000\003" + "\003\000foo" + "emacs\000" + "emacs\000" + "\001\002" + "XXX", + 32, 3 // FNAME of "emacs", FCOMMENT of "emacs", FHCRC, "foo", crud + }, + }; + + // Test all the headers test cases. + for (int i = 0; i < ABSL_ARRAYSIZE(tests); ++i) { + // Test many random ways they might be fragmented. + for (int j = 0; j < 100 * 1000; ++j) { + // Get the test case set up. + const char* p = tests[i].str; + int bytes_left = tests[i].len; + int bytes_read = 0; + + // Pick some random places to fragment the headers. + const int num_fragments = rnd.Uniform(bytes_left); + std::vector fragment_starts; + for (int frag_num = 0; frag_num < num_fragments; ++frag_num) { + fragment_starts.push_back(rnd.Uniform(bytes_left)); + } + std::sort(fragment_starts.begin(), fragment_starts.end()); + + VLOG(1) << "====="; + GZipHeader gzip_headers; + // Go through several fragments and pass them to the headers for parsing. + int frag_num = 0; + while (bytes_left > 0) { + const int fragment_len = (frag_num < num_fragments) + ? (fragment_starts[frag_num] - bytes_read) + : (tests[i].len - bytes_read); + CHECK_GE(fragment_len, 0); + const char* header_end = NULL; + VLOG(1) << absl::StrFormat("Passing %2d bytes at %2d..%2d: %s", + fragment_len, bytes_read, + bytes_read + fragment_len, + absl::CEscape(std::string(p, fragment_len))); + GZipHeader::Status status = + gzip_headers.ReadMore(p, fragment_len, &header_end); + bytes_read += fragment_len; + bytes_left -= fragment_len; + CHECK_GE(bytes_left, 0); + p += fragment_len; + frag_num++; + if (bytes_left <= tests[i].cruft_len) { + CHECK_EQ(status, GZipHeader::COMPLETE_HEADER); + break; + } else { + CHECK_EQ(status, GZipHeader::INCOMPLETE_HEADER); + } + } // while + } // for many fragmentations + } // for all test case headers +} + +} // namespace firebase diff --git a/app/rest/tests/request_binary_test.cc b/app/rest/tests/request_binary_test.cc new file mode 100644 index 0000000000..2d99bb6581 --- /dev/null +++ b/app/rest/tests/request_binary_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Google LLC + * + * 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 +#include + +#include "app/rest/request_binary.h" +#include "app/rest/request_binary_gzip.h" +#include "app/rest/request_options.h" +#include "app/rest/zlibwrapper.h" +#include "app/rest/tests/request_test.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +class RequestBinaryTest : public ::testing::Test { + protected: + // Codec that decompresses a gzip encoded string. + static std::string Decompress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_length = zlib.GzipUncompressedLength( + reinterpret_cast(input.data()), input.length()); + std::unique_ptr result(new char[result_length]); + int err = zlib.Uncompress( + reinterpret_cast(result.get()), &result_length, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_length); + } +}; + +TEST_F(RequestBinaryTest, GetSmallPostFields) { + TestCreateAndReadRequestBody(kSmallString, + sizeof(kSmallString)); +} + +TEST_F(RequestBinaryTest, GetLargePostFields) { + std::string large_buffer = CreateLargeTextData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +TEST_F(RequestBinaryTest, GetSmallBinaryPostFields) { + TestCreateAndReadRequestBody(kSmallBinary, + sizeof(kSmallBinary)); +} + +TEST_F(RequestBinaryTest, GetLargeBinaryPostFields) { + std::string large_buffer = CreateLargeBinaryData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +TEST_F(RequestBinaryTest, GetSmallPostFieldsWithGzip) { + TestCreateAndReadRequestBody( + kSmallString, sizeof(kSmallString), Decompress); +} + +TEST_F(RequestBinaryTest, GetLargePostFieldsWithGzip) { + std::string large_buffer = CreateLargeTextData(); + TestCreateAndReadRequestBody( + large_buffer.c_str(), large_buffer.size(), Decompress); +} + +TEST_F(RequestBinaryTest, GetSmallBinaryPostFieldsWithGzip) { + TestCreateAndReadRequestBody( + kSmallBinary, sizeof(kSmallBinary), Decompress); +} + +TEST_F(RequestBinaryTest, GetLargeBinaryPostFieldsWithGzip) { + std::string large_buffer = CreateLargeBinaryData(); + TestCreateAndReadRequestBody( + large_buffer.c_str(), large_buffer.size(), Decompress); +} + +} // namespace test +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_file_test.cc b/app/rest/tests/request_file_test.cc new file mode 100644 index 0000000000..64ceb3f2bb --- /dev/null +++ b/app/rest/tests/request_file_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2018 Google LLC + * + * 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 +#include + +#include "app/rest/request_file.h" +#include "app/rest/tests/request_test.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +class RequestFileTest : public ::testing::Test { + public: + RequestFileTest() + : filename_(FLAGS_test_tmpdir + "/a_file.txt"), + file_(nullptr), + file_size_(0) {} + + void SetUp() override; + void TearDown() override; + + protected: + std::string filename_; + FILE* file_; + size_t file_size_; + + static const char kFileContents[]; +}; + +const char RequestFileTest::kFileContents[] = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " + "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim " + "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "aliquip ex ea commodo consequat. Duis aute irure dolor in " + "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " + "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."; + +void RequestFileTest::SetUp() { + file_size_ = sizeof(kFileContents) - 1; + file_ = fopen(filename_.c_str(), "wb"); + CHECK(file_ != nullptr); + CHECK_EQ(file_size_, fwrite(kFileContents, 1, file_size_, file_)); + CHECK_EQ(0, fclose(file_)); +} + +void RequestFileTest::TearDown() { CHECK_EQ(0, unlink(filename_.c_str())); } + +TEST_F(RequestFileTest, NonExistentFile) { + RequestFile request("a_file_that_doesnt_exist.txt", 0); + EXPECT_FALSE(request.IsFileOpen()); +} + +TEST_F(RequestFileTest, OpenFile) { + RequestFile request(filename_.c_str(), 0); + EXPECT_TRUE(request.IsFileOpen()); +} + +TEST_F(RequestFileTest, GetFileSize) { + RequestFile request(filename_.c_str(), 0); + EXPECT_EQ(file_size_, request.file_size()); +} + +TEST_F(RequestFileTest, ReadFile) { + RequestFile request(filename_.c_str(), 0); + EXPECT_EQ(kFileContents, ReadRequestBody(&request)); +} + +TEST_F(RequestFileTest, ReadFileFromOffset) { + size_t read_offset = 29; + RequestFile request(filename_.c_str(), read_offset); + EXPECT_EQ(&kFileContents[read_offset], ReadRequestBody(&request)); +} + +} // namespace test +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_json_test.cc b/app/rest/tests/request_json_test.cc new file mode 100644 index 0000000000..1875c1447e --- /dev/null +++ b/app/rest/tests/request_json_test.cc @@ -0,0 +1,71 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/request_json.h" +#include "app/rest/sample_generated.h" +#include "app/rest/sample_resource.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class RequestSample : public RequestJson { + public: + RequestSample() : RequestJson(sample_resource_data) {} + + void set_token(const char* token) { + application_data_->token = token; + UpdatePostFields(); + } + + void set_number(int number) { + application_data_->number = number; + UpdatePostFields(); + } + + void UpdatePostFieldForTest() { + UpdatePostFields(); + } +}; + +// Test the creation. +TEST(RequestJsonTest, Creation) { + RequestSample request; + EXPECT_TRUE(request.options().post_fields.empty()); +} + +// Test the case where no field is set. +TEST(RequestJsonTest, UpdatePostFieldsEmpty) { + RequestSample request; + request.UpdatePostFieldForTest(); + EXPECT_EQ("{\n" + "}\n", request.options().post_fields); +} + +// Test with fields set. +TEST(RequestJsonTest, UpdatePostFields) { + RequestSample request; + request.set_number(123); + request.set_token("abc"); + EXPECT_EQ("{\n" + " token: \"abc\",\n" + " number: 123\n" + "}\n", request.options().post_fields); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_test.cc b/app/rest/tests/request_test.cc new file mode 100644 index 0000000000..2fcd2238bf --- /dev/null +++ b/app/rest/tests/request_test.cc @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/request.h" + +#include "app/rest/tests/request_test.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +TEST(RequestTest, SetUrl) { + Request request; + EXPECT_EQ("", request.options().url); + + request.set_url("some.url"); + EXPECT_EQ("some.url", request.options().url); +} + +TEST(RequestTest, GetSmallPostFields) { + TestCreateAndReadRequestBody(kSmallString, sizeof(kSmallString)); +} + +TEST(RequestTest, GetLargePostFields) { + std::string large_buffer = CreateLargeTextData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +TEST(RequestTest, GetSmallBinaryPostFields) { + TestCreateAndReadRequestBody(kSmallBinary, sizeof(kSmallBinary)); +} + +TEST(RequestTest, GetLargeBinaryPostFields) { + std::string large_buffer = CreateLargeBinaryData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +} // namespace test +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_test.h b/app/rest/tests/request_test.h new file mode 100644 index 0000000000..e2b0b87669 --- /dev/null +++ b/app/rest/tests/request_test.h @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +#ifndef FIREBASE_APP_CLIENT_CPP_REST_TESTS_REQUEST_TEST_H_ +#define FIREBASE_APP_CLIENT_CPP_REST_TESTS_REQUEST_TEST_H_ + +#include +#include +#include +#include + +#include "app/rest/request.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +const char kSmallString[] = "hello world"; +const char kSmallBinary[] = {'a', 'b', '\0', 'c', '\0', 'x', 'y', 'z'}; +const size_t kLargeDataSize = 10 * 1024 * 1024; + +// Read data from a request into a string. +static std::string ReadRequestBody(Request* request) { + std::string output; + EXPECT_TRUE(request->ReadBodyIntoString(&output)); + return output; +} + +// No-op codec method that returns the specified string. +static std::string NoCodec(const std::string& string_to_decode) { + return string_to_decode; +} + +// Test creating and reading from a request. +template +void TestCreateAndReadRequestBody( + const char* buffer, size_t size, + std::function codec = NoCodec) { + { + // Test read without copying into the request. + std::vector modified_expected(buffer, buffer + size); + std::vector copy(modified_expected); + T request(©[0], copy.size()); + // Modify the buffer to validate it wasn't copied by the request. + for (size_t i = 0; i < size; ++i) { + copy[i]++; + modified_expected[i]++; + } + EXPECT_EQ(std::string(&modified_expected[0], size), + codec(ReadRequestBody(&request))); + } + { + const std::string expected(buffer, size); + T request; + { + // This allocates the string on the heap to ensure the memory is stomped + // with a pattern when deallocated in debug mode. + // Same below. + std::unique_ptr copy(new std::string(expected)); + request.set_post_fields(copy->c_str(), copy->length()); + } + EXPECT_EQ(expected, codec(ReadRequestBody(&request))); + } + { + const std::string expected(buffer); + T request; + { + std::unique_ptr copy(new std::string(expected)); + request.set_post_fields(copy->c_str()); + } + EXPECT_EQ(expected, codec(ReadRequestBody(&request))); + } +} + +// Create a random data stream of characters 0-9. +static const std::string CreateLargeTextData() { + std::string s; + unsigned int seed = 0; + srand(seed); + for (size_t i = 0; i < kLargeDataSize; i++) { + s += '0' + (rand() % 10); // NOLINT (rand_r() doesn't work on MSVC) + } + return s; +} + +// Create a random stream of binary data. +static const std::string CreateLargeBinaryData() { + std::string s; + unsigned int seed = 0; + srand(seed); + for (size_t i = 0; i < kLargeDataSize; i++) { + s += static_cast(rand()); // NOLINT (rand_r() doesn't work on MSVC) + } + return s; +} + +} // namespace test +} // namespace rest +} // namespace firebase + +#endif // FIREBASE_APP_CLIENT_CPP_REST_TESTS_REQUEST_TEST_H_ diff --git a/app/rest/tests/response_binary_test.cc b/app/rest/tests/response_binary_test.cc new file mode 100644 index 0000000000..8ec8e72bc1 --- /dev/null +++ b/app/rest/tests/response_binary_test.cc @@ -0,0 +1,128 @@ +/* + * Copyright 2017 Google LLC + * + * 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 + +#include "app/rest/response_binary.h" +#include "app/rest/zlibwrapper.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class ResponseBinaryTest : public ::testing::Test { + protected: + std::string Compress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_size = ZLib::MinCompressbufSize(input.length()); + std::unique_ptr result(new char[result_size]); + int err = zlib.Compress( + reinterpret_cast(result.get()), &result_size, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_size); + } + + std::string GetBody() { + const char* data; + size_t size; + response_.GetBody(&data, &size); + return std::string(data, size); + } + + void SetBody(const std::string& body) { + response_.ProcessBody(body.data(), body.length()); + response_.MarkCompleted(); + } + + ResponseBinary response_; +}; + +TEST_F(ResponseBinaryTest, GetBodyWihoutGunzip) { + std::string s = "hello world"; + SetBody(s); + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBinaryBodyWihoutGunzip) { + char buffer[] = {'a', 'b', '\0', 'c', '\0', 'x', 'y', 'z'}; + std::string s(buffer, sizeof(buffer)); + SetBody(s); + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBodyWihGunzip) { + response_.set_use_gunzip(true); + + std::string s = "hello world"; + SetBody(Compress(s)); + + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBinaryBodyWihGunzip) { + response_.set_use_gunzip(true); + + char buffer[] = {'a', 'b', '\0', 'c', '\0', 'x', 'y', 'z'}; + std::string s(buffer, sizeof(buffer)); + SetBody(Compress(s)); + + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBodyWihGunzipHugeBuffer) { + response_.set_use_gunzip(true); + + // 10 MB body + std::string s; + unsigned int seed = 0; + srand(seed); + size_t size = 10 * 1024 * 1024; + for (size_t i = 0; i < size; i++) { + s += '0' + (rand() % 10); // NOLINT (rand_r() doesn't work on windows) + } + SetBody(Compress(s)); + + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBinaryBodyWihGunzipHugeBuffer) { + response_.set_use_gunzip(true); + + // 10 MB body + size_t size = 10 * 1024 * 1024; + char* buffer = new char[size]; + + unsigned int seed = 0; + srand(seed); + for (size_t i = 0; i < size; i++) { + // Add 0-9 numbers and '\0' to buffer. + buffer[i] = (i % 10) ? ('0' + (rand() % 10)): '\0'; // NOLINT + // (no rand_r on msvc) + } + + std::string s(buffer, sizeof(buffer)); + SetBody(Compress(s)); + EXPECT_EQ(GetBody(), s); + + delete[] buffer; +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/response_json_test.cc b/app/rest/tests/response_json_test.cc new file mode 100644 index 0000000000..0f2f94f3f7 --- /dev/null +++ b/app/rest/tests/response_json_test.cc @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/response_json.h" +#include +#include "app/rest/sample_generated.h" +#include "app/rest/sample_resource.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class ResponseSample : public ResponseJson { + public: + ResponseSample() : ResponseJson(sample_resource_data) {} + ResponseSample(ResponseSample&& rhs) : ResponseJson(std::move(rhs)) {} + + std::string token() const { + return application_data_ ? application_data_->token : std::string(); + } + + int number() const { + return application_data_ ? application_data_->number : 0; + } +}; + +// Test the creation. +TEST(ResponseJsonTest, Creation) { + ResponseSample response; + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); +} + +// Test move operation. +TEST(ResponseJsonTest, Move) { + ResponseSample src; + const char body[] = + "{" + " \"token\": \"abc\"," + " \"number\": 123" + "}"; + src.ProcessBody(body, sizeof(body)); + src.MarkCompleted(); + const auto check_non_empty = [](const ResponseSample& response) { + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_EQ("abc", response.token()); + EXPECT_EQ(123, response.number()); + }; + check_non_empty(src); + + ResponseSample dest = std::move(src); + // src should now be moved-from and its parsed fields should be blank. + // NOLINT disables ClangTidy checks that warn about access to moved-from + // object. In this case, this is deliberate. The only data member that gets + // accessed is application_data_, which is std::unique_ptr and has + // well-defined state (equivalent to default-created). + EXPECT_TRUE(src.token().empty()); // NOLINT + EXPECT_EQ(0, src.number()); // NOLINT + // dest should now contain everything src contained. + check_non_empty(dest); +} + +// Test the case server respond with just {}. +TEST(ResponseJsonTest, EmptyJsonResponse) { + ResponseSample response; + const char body[] = + "{" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_TRUE(response.token().empty()); + EXPECT_EQ(0, response.number()); +} + +// Test the case server respond with non-empty standard JSON string. +TEST(ResponseJsonTest, StandardJsonResponse) { + ResponseSample response; + const char body[] = + "{" + " \"token\": \"abc\"," + " \"number\": 123" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_EQ("abc", response.token()); + EXPECT_EQ(123, response.number()); +} + +// Test the case server respond with non-empty JSON string. +TEST(ResponseJsonTest, NonStandardJsonResponse) { + ResponseSample response; + // JSON format has non-standard variations: + // quotation around field name or not; + // quotation around non-string field value or not; + // single quotes vs double quotes + // Here we try some of the non-standard variations. + const char body[] = + "{" + " token: 'abc'," + " 'number': '123'" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_EQ("abc", response.token()); + EXPECT_EQ(123, response.number()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/response_test.cc b/app/rest/tests/response_test.cc new file mode 100644 index 0000000000..00adb7d159 --- /dev/null +++ b/app/rest/tests/response_test.cc @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/response.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +// A helper function that prepares char buffer and calls the response's +// ProcessHeader. Param str must be a C string. +void ProcessHeader(const char* str, Response* response) { + // Prepare the char buffer to call ProcessHeader and make sure the + // implementation does not rely on that buffer is \0 terminated. + size_t length = strlen(str); + char* buffer = new char[length + 20]; // We pad the buffer with a few '#'. + memset(buffer, '#', length + 20); + memcpy(buffer, str, length); // Intentionally not copy the \0. + + // Now call the ProcessHeader. + response->ProcessHeader(buffer, length); + delete[] buffer; +} + +TEST(ResponseTest, ProcessStatusLine) { + Response response; + EXPECT_EQ(0, response.status()); + + ProcessHeader("HTTP/1.1 200 OK\r\n", &response); + EXPECT_EQ(200, response.status()); + + ProcessHeader("HTTP/1.1 302 Found\r\n", &response); + EXPECT_EQ(302, response.status()); +} + +TEST(ResponseTest, ProcessHeaderEnding) { + Response response; + EXPECT_FALSE(response.header_completed()); + + ProcessHeader("HTTP/1.1 200 OK\r\n", &response); + EXPECT_FALSE(response.header_completed()); + + ProcessHeader("\r\n", &response); + EXPECT_TRUE(response.header_completed()); +} + +TEST(ResponseTest, ProcessHeaderField) { + Response response; + EXPECT_STREQ(nullptr, response.GetHeader("Content-Type")); + EXPECT_STREQ(nullptr, response.GetHeader("Date")); + EXPECT_STREQ(nullptr, response.GetHeader("key")); + + ProcessHeader("Content-Type: text/html; charset=UTF-8\r\n", &response); + ProcessHeader("Date: Wed, 05 Jul 2017 15:55:19 GMT\r\n", &response); + ProcessHeader("key: value\r\n", &response); + EXPECT_STREQ("text/html; charset=UTF-8", response.GetHeader("Content-Type")); + EXPECT_STREQ("Wed, 05 Jul 2017 15:55:19 GMT", response.GetHeader("Date")); + EXPECT_STREQ("value", response.GetHeader("key")); +} + +// Below test the fetch-time logic for various test cases. +TEST(ResponseTest, ProcessDateHeaderValidDate) { + Response response; + EXPECT_EQ(0, response.fetch_time()); + ProcessHeader("Date: Wed, 05 Jul 2017 15:55:19 GMT\r\n", &response); + response.MarkCompleted(); + EXPECT_EQ(1499270119, response.fetch_time()); +} + +TEST(ResponseTest, ProcessDateHeaderInvalidDate) { + Response response; + EXPECT_EQ(0, response.fetch_time()); + ProcessHeader("Date: here is a invalid date\r\n", &response); + response.MarkCompleted(); + EXPECT_LT(1499270119, response.fetch_time()); +} + +TEST(ResponseTest, ProcessDateHeaderMissing) { + Response response; + EXPECT_EQ(0, response.fetch_time()); + response.MarkCompleted(); + EXPECT_LT(1499270119, response.fetch_time()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/testdata/sample.fbs b/app/rest/tests/testdata/sample.fbs new file mode 100644 index 0000000000..332255f22b --- /dev/null +++ b/app/rest/tests/testdata/sample.fbs @@ -0,0 +1,24 @@ +// Copyright 2017 Google LLC +// +// 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. + +// A simple FlatBuffer schema as a sample. + +namespace firebase.rest; + +table Sample { + token:string; + number:int; +} + +root_type Sample; diff --git a/app/rest/tests/transport_curl_test.cc b/app/rest/tests/transport_curl_test.cc new file mode 100644 index 0000000000..b484f14765 --- /dev/null +++ b/app/rest/tests/transport_curl_test.cc @@ -0,0 +1,180 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +// This is a large test that starts a local http server and tests transport_curl +// with actual http connection. + +#include "app/rest/transport_curl.h" + +#include +#include + +#include "app/rest/request.h" +#include "app/rest/response.h" +#include "net/http2/server/lib/public/httpserver2.h" +#include "net/util/ports.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "absl/strings/str_format.h" +#include "absl/synchronization/mutex.h" +#include "absl/time/time.h" +#include "util/task/status.h" + +namespace firebase { +namespace rest { + +const char* kServerVersion = "HTTP server for test"; + +void UriHandler(HTTPServerRequest* request) { + if (request->http_method() == "GET") { + request->output()->WriteString("test"); + request->Reply(); + LOG(INFO) << "Sent response for GET"; + } else if (request->http_method() == "POST" && + request->input_headers()->HeaderIs("Content-Type", + "application/json")) { + request->output()->WriteString(request->input()->ToString()); + request->Reply(); + LOG(INFO) << "Sent response for POST"; + } else { + FAIL(); + } +} + +const absl::Duration kTimeoutSeconds = absl::Seconds(10); + +class TestResponse : public Response { + public: + void MarkCompleted() override { + absl::MutexLock lock(&mutex_); + Response::MarkCompleted(); + } + + void Wait() { + absl::MutexLock lock(&mutex_); + mutex_.AwaitWithTimeout( + absl::Condition( + [](void* userdata) -> bool { + auto* response = static_cast(userdata); + return response->header_completed() && response->body_completed(); + }, + this), + kTimeoutSeconds); + } + + private: + absl::Mutex mutex_; +}; + +class TransportCurlTest : public testing::Test { + protected: + static void SetUpTestSuite() { + InitTransportCurl(); + // Start a local http server for testing the http request. + // Pick up a port. + std::string error; // PickUnusedPort actually asks for google3 string. + TransportCurlTest::port_ = net_util::PickUnusedPort(&error); + CHECK_GE(TransportCurlTest::port_, 0) << error; + LOG(INFO) << "Auto selected port " << port_ << " for test http server"; + // Create a new server. + std::unique_ptr options( + new net_http2::HTTPServer2::EventModeOptions()); + options->SetVersion(kServerVersion); + options->SetDataVersion("data_1.0"); + options->SetServerType("server"); + options->AddPort(TransportCurlTest::port_); + options->SetWindowSizesAndLatency(0, 0, true); + auto creation_status = net_http2::HTTPServer2::CreateEventDrivenModeServer( + nullptr /* event manager */, std::move(options)); + CHECK_OK(creation_status.status()); + // Register URI handler and start serving. + TransportCurlTest::server_ = creation_status.value().release(); + ABSL_DIE_IF_NULL(TransportCurlTest::server_) + ->RegisterHandler("*", NewPermanentCallback(&UriHandler)); + CHECK_OK(TransportCurlTest::server_->StartAcceptingRequests()); + LOG(INFO) << "Local HTTP server is ready to accept request"; + } + static void TearDownTestSuite() { + TransportCurlTest::server_->TerminateServer(); + delete TransportCurlTest::server_; + TransportCurlTest::server_ = nullptr; + CleanupTransportCurl(); + } + static int32 port_; + static net_http2::HTTPServer2* server_; +}; + +int32 TransportCurlTest::port_; +net_http2::HTTPServer2* TransportCurlTest::server_; + +TEST_F(TransportCurlTest, TestGlobalInitAndCleanup) { + InitTransportCurl(); + CleanupTransportCurl(); +} + +TEST_F(TransportCurlTest, TestCreation) { TransportCurl curl; } + +TEST_F(TransportCurlTest, TestHttpGet) { + Request request; + request.set_verbose(true); + TestResponse response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + EXPECT_EQ(nullptr, response.GetHeader("Server")); + EXPECT_STREQ("", response.GetBody()); + + const std::string& url = + absl::StrFormat("http://localhost:%d", TransportCurlTest::port_); + request.set_url(url.c_str()); + TransportCurl curl; + curl.Perform(request, &response); + response.Wait(); + EXPECT_EQ(200, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_STREQ(kServerVersion, response.GetHeader("Server")); + EXPECT_STREQ("test", response.GetBody()); +} + +TEST_F(TransportCurlTest, TestHttpPost) { + Request request; + request.set_verbose(true); + TestResponse response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + EXPECT_EQ(nullptr, response.GetHeader("Server")); + EXPECT_STREQ("", response.GetBody()); + + const std::string& url = + absl::StrFormat("http://localhost:%d", TransportCurlTest::port_); + request.set_url(url.c_str()); + request.set_method("POST"); + request.add_header("Content-Type", "application/json"); + request.set_post_fields("{'a':'a','b':'b'}"); + TransportCurl curl; + curl.Perform(request, &response); + response.Wait(); + EXPECT_EQ(200, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_STREQ(kServerVersion, response.GetHeader("Server")); + EXPECT_STREQ("{'a':'a','b':'b'}", response.GetBody()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/transport_mock_test.cc b/app/rest/tests/transport_mock_test.cc new file mode 100644 index 0000000000..ea759dd33b --- /dev/null +++ b/app/rest/tests/transport_mock_test.cc @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/transport_mock.h" +#include "app/rest/request.h" +#include "app/rest/response.h" +#include "testing/config.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +TEST(TransportMockTest, TestCreation) { TransportMock mock; } + +TEST(TransportMockTest, TestHttpGet200) { + Request request; + Response response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + EXPECT_EQ(nullptr, response.GetHeader("Server")); + EXPECT_STREQ("", response.GetBody()); + + request.set_url("http://my.fake.site"); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'http://my.fake.site'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['this is a mock',]" + " }" + " }" + " ]" + "}"); + TransportMock transport; + transport.Perform(request, &response); + EXPECT_EQ(200, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_STREQ("mock server 101", response.GetHeader("Server")); + EXPECT_STREQ("this is a mock", response.GetBody()); +} + +TEST(TransportMockTest, TestHttpGet404) { + Request request; + Response response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + + request.set_url("http://my.fake.site"); + firebase::testing::cppsdk::ConfigSet("{config:[]}"); + TransportMock transport; + transport.Perform(request, &response); + EXPECT_EQ(404, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/util_test.cc b/app/rest/tests/util_test.cc new file mode 100644 index 0000000000..a7f5cff05c --- /dev/null +++ b/app/rest/tests/util_test.cc @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace util { + +TEST(UtilTest, TestTrimWhitespace) { + // Empty + EXPECT_EQ("", TrimWhitespace("")); + // Only white space + EXPECT_EQ("", TrimWhitespace(" ")); + EXPECT_EQ("", TrimWhitespace(" \r\n \t ")); + // A single letter + EXPECT_EQ("x", TrimWhitespace(" x")); + EXPECT_EQ("x", TrimWhitespace("x ")); + EXPECT_EQ("x", TrimWhitespace(" x ")); + // A word + EXPECT_EQ("abc", TrimWhitespace("\t abc")); + EXPECT_EQ("abc", TrimWhitespace("abc \r\n")); + EXPECT_EQ("abc", TrimWhitespace("\t abc \r\n")); + // A few words + EXPECT_EQ("mary had little lamb", TrimWhitespace(" mary had little lamb")); + EXPECT_EQ("mary had little lamb", TrimWhitespace("mary had little lamb ")); + EXPECT_EQ("mary had little lamb", TrimWhitespace(" mary had little lamb ")); +} + +TEST(UtilTest, TestToUpper) { + // Empty + EXPECT_EQ("", ToUpper("")); + // Only non-alpha characters + EXPECT_EQ("3.1415926", ToUpper("3.1415926")); + // Letters + EXPECT_EQ("A", ToUpper("a")); + EXPECT_EQ("ABC", ToUpper("AbC")); + // Mixed + EXPECT_EQ("789 ABC", ToUpper("789 abc")); + EXPECT_EQ("1A2B3C", ToUpper("1a2b3c")); +} + +} // namespace util +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/www_form_url_encoded_test.cc b/app/rest/tests/www_form_url_encoded_test.cc new file mode 100644 index 0000000000..6aaa6ceb3b --- /dev/null +++ b/app/rest/tests/www_form_url_encoded_test.cc @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/rest/www_form_url_encoded.h" + +#include "app/rest/util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class WwwFormUrlEncodedTest : public ::testing::Test { + protected: + void SetUp() override { util::Initialize(); } + + void TearDown() override { util::Terminate(); } +}; + +TEST_F(WwwFormUrlEncodedTest, Initialize) { + std::string initial("something"); + WwwFormUrlEncoded form(&initial); + EXPECT_EQ(initial, form.form_data()); +} + +TEST_F(WwwFormUrlEncodedTest, AddFields) { + std::string form_data; + WwwFormUrlEncoded form(&form_data); + form.Add("foo", "bar"); + form.Add("bash", "bish bosh"); + form.Add("h:&=l\nlo", "g@@db=\r\tye&\xfe"); + form.Add(WwwFormUrlEncoded::Item("hip", "hop")); + EXPECT_EQ("foo=bar&bash=bish%20bosh&" + "h%3A%26%3Dl%0Alo=g%40%40db%3D%0D%09ye%26%FE&" + "hip=hop", + form.form_data()); +} + +TEST_F(WwwFormUrlEncodedTest, ParseEmpty) { + auto items = WwwFormUrlEncoded::Parse(""); + EXPECT_EQ(0, items.size()); +} + +TEST_F(WwwFormUrlEncodedTest, ParseForm) { + WwwFormUrlEncoded::Item expected_items[] = { + WwwFormUrlEncoded::Item("h:llo", "g@@dbye&"), + WwwFormUrlEncoded::Item("bash", "bish bosh"), + }; + auto items = WwwFormUrlEncoded::Parse( + "h%3Allo=g%40%40dbye%26&" + "bash=bish%20bosh"); + EXPECT_EQ(sizeof(expected_items) / sizeof(expected_items[0]), items.size()); + for (size_t i = 0; i < items.size(); ++i) { + EXPECT_EQ(expected_items[i].key, items[i].key) << "Key " << i; + EXPECT_EQ(expected_items[i].value, items[i].value) << "Value " << i; + } +} + +TEST_F(WwwFormUrlEncodedTest, ParseFormWithOtherSeparators) { + WwwFormUrlEncoded::Item expected_items[] = { + WwwFormUrlEncoded::Item("h:llo", "g@@dbye&"), + WwwFormUrlEncoded::Item("bash", "bish bosh"), + WwwFormUrlEncoded::Item("hello", "you"), + }; + auto items = WwwFormUrlEncoded::Parse( + "h%3Allo=g%40%40dbye%26&\r " + "bash=bish%20bosh\n&\t&\nhello=you"); + EXPECT_EQ(sizeof(expected_items) / sizeof(expected_items[0]), items.size()); + for (size_t i = 0; i < items.size(); ++i) { + EXPECT_EQ(expected_items[i].key, items[i].key) << "Key " << i; + EXPECT_EQ(expected_items[i].value, items[i].value) << "Value " << i; + } +} + +TEST_F(WwwFormUrlEncodedTest, ParseFormWithInvalidFields) { + WwwFormUrlEncoded::Item expected_items[] = { + WwwFormUrlEncoded::Item("h:llo", "g@@dbye&"), + WwwFormUrlEncoded::Item("bash", "bish bosh"), + }; + auto items = WwwFormUrlEncoded::Parse( + "h%3Allo=g%40%40dbye%26&" + "invalidfield0&" + "bash=bish%20bosh&" + "moreinvaliddata&" + "ignorethisaswell"); + EXPECT_EQ(sizeof(expected_items) / sizeof(expected_items[0]), items.size()); + for (size_t i = 0; i < items.size(); ++i) { + EXPECT_EQ(expected_items[i].key, items[i].key) << "Key " << i; + EXPECT_EQ(expected_items[i].value, items[i].value) << "Value " << i; + } +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/zlibwrapper_unittest.cc b/app/rest/tests/zlibwrapper_unittest.cc new file mode 100644 index 0000000000..9d8e31d62f --- /dev/null +++ b/app/rest/tests/zlibwrapper_unittest.cc @@ -0,0 +1,1050 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/rest/zlibwrapper.h" + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "absl/base/macros.h" +#include "absl/strings/escaping.h" +#include "util/random/acmrandom.h" + +// 1048576 == 2^20 == 1 MB +#define MAX_BUF_SIZE 1048500 +#define MAX_BUF_FLEX 1048576 + +DEFINE_int32(min_comp_lvl, 6, "Minimum compression level"); +DEFINE_int32(max_comp_lvl, 6, "Maximum compression level"); +DEFINE_string(dict, "", "Dictionary file to use (overrides default text)"); +DEFINE_string(files_to_process, "", + "Comma separated list of filenames to read in for our tests. " + "If empty, a default file from testdata is used."); +DEFINE_int32(zlib_max_size_uncompressed_data, 10 * 1024 * 1024, // 10MB + "Maximum expected size of the uncompress length " + "in the gzip footer."); +DEFINE_string(read_past_window_data_file, "", + "Data to use for reproducing read-past-window bug;" + " defaults to zlib/testdata/read_past_window.data"); +DEFINE_int32(read_past_window_iterations, 4000, + "Number of attempts to read past end of window"); +ABSL_FLAG(absl::Duration, slow_test_deadline, absl::Minutes(2), + "The voluntary time limit some of the slow tests attempt to " + "adhere to. Used only if the build is detected as an unusually " + "slow one according to ValgrindSlowdown(). Set to \"inf\" to " + "disable."); + +namespace firebase { + +namespace { + +// A helper class for build configurations that really slow down the build. +// +// Some of this file's tests are so CPU intensive that they no longer +// finish in a reasonable time under "sanitizer" builds. These builds +// advertise themselves with a ValgrindSlowdown() > 1.0. Use this class to +// abandon tests after reasonable deadlines. +class SlowTestLimiter { + public: + // Initializes the deadline relative to absl::Now(). + SlowTestLimiter(); + + // A human readable reason for the limiter's policy. + std::string reason() { return reason_; } + + // Returns true if this known to be a slow build. + bool IsSlowBuild() const { return deadline_ < absl::InfiniteFuture(); } + + // Returns true iff absl::Now() > deadline(). This class is passive; the + // test must poll. + bool DeadlineExceeded() const { return absl::Now() > deadline_; } + + private: + std::string reason_; + absl::Time deadline_; +}; + +SlowTestLimiter::SlowTestLimiter() { + deadline_ = absl::InfiniteFuture(); + double slowdown = ValgrindSlowdown(); + reason_ = + absl::StrCat("ValgrindSlowdown() of ", absl::LegacyPrecision(slowdown)); + if (slowdown <= 1.0) return; + absl::Duration relative_deadline = absl::GetFlag(FLAGS_slow_test_deadline); + absl::StrAppend(&reason_, " with --slow_test_deadline=", + absl::FormatDuration(relative_deadline)); + deadline_ = absl::Now() + relative_deadline; +} + +REGISTER_MODULE_INITIALIZER(zlibwrapper_unittest, { + SlowTestLimiter limiter; + LOG(WARNING) + << "SlowTestLimiter policy " + << (limiter.IsSlowBuild() + ? "limited; slow tests will voluntarily limit execution time." + : "unlimited.") + << " Reason: " << limiter.reason(); +}); + +bool ReadFileToString(const std::string& filename, std::string* output, + int64 max_size) { + std::ifstream f; + f.open(filename); + if (f.fail()) { + return false; + } + f.seekg(0, std::ios::end); + int64 length = std::min(static_cast(f.tellg()), max_size); + f.seekg(0, std::ios::beg); + output->resize(length); + f.read(&*output->begin(), length); + f.close(); + return !f.fail(); +} + +void TestCompression(ZLib* zlib, const std::string& uncompbuf, + const char* msg) { + LOG(INFO) << "TestCompression of " << uncompbuf.size() << " bytes."; + + uLongf complen = ZLib::MinCompressbufSize(uncompbuf.size()); + std::string compbuf(complen, '\0'); + int err = zlib->Compress((Bytef*)compbuf.data(), &complen, + (Bytef*)uncompbuf.data(), uncompbuf.size()); + EXPECT_EQ(Z_OK, err) << " " << uncompbuf.size() << " bytes down to " + << complen << " bytes."; + + // Output data size should match input data size. + uLongf uncomplen2 = uncompbuf.size(); + std::string uncompbuf2(uncomplen2, '\0'); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + + if (msg != nullptr) { + printf("Orig: %7lu Compressed: %7lu %5.3f %s\n", uncomplen2, complen, + (float)complen / uncomplen2, msg); + } + + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; +} + +// Due to a bug in old versions of zlibwrapper, we appended the gzip +// footer even in non-gzip mode. This tests that we can correctly +// uncompress this buggily-compressed data. +void TestBuggyCompression(ZLib* zlib, const std::string& uncompbuf) { + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + uLongf complen = compbuf.size(); + int err = zlib->Compress((Bytef*)compbuf.data(), &complen, + (Bytef*)uncompbuf.data(), uncompbuf.size()); + EXPECT_EQ(Z_OK, err) << " " << uncompbuf.size() << " bytes down to " + << complen << " bytes."; + + complen += 8; // 8 bytes is size of gzip footer + + uLongf uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // Make sure uncompress-chunk works as well + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + ASSERT_TRUE(zlib->UncompressChunkDone()); + + // Try to uncompress an incomplete chunk (missing 4 bytes from the + // gzip header, which we're ignoring). + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen - 4); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // Repeat UncompressChunk with the rest of the gzip header. + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data() + complen - 4, 4); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(0, uncomplen2); + + ASSERT_TRUE(zlib->UncompressChunkDone()); + + // Uncompress works on a complete input, so it should be able to + // assume that either the gzip footer is all there or its not there at all. + // Make sure it doesn't work on things that don't look like gzip footers. + complen -= 4; // now we're smaller than the footer size + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_DATA_ERROR, err); + + complen += 8; // now we're bigger than the footer size + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_DATA_ERROR, err); +} + +// Make sure we uncompress right even if the first chunk is in the middle +// of the gzip headers, or in the middle of the gzip footers. (TODO) +void TestGzipHeaderUncompress(ZLib* zlib) { + struct { + const char* s; + int len; + int level; + } comp_chunks[][10] = { + // Level 0: no gzip footer (except partial footer for the last case) + // Level 1: normal gzip footer + // Level 2: extra byte after gzip footer + { + // divide up: header, body ("hello, world!\n"), footer + {"\037\213\010\000\216\176\356\075\002\003", 10, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266\016\000\000\000", 8, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: partial header, partial header, + // body ("hello, world!\n"), footer + {"\037\213\010\000\216", 5, 0}, + {"\176\356\075\002\003", 5, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266\016\000\000\000", 8, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: full header, + // body ("hello, world!\n"), partial footer, partial footer + {"\037\213\010\000\216\176\356\075\002\003", 10, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266", 4, 1}, + {"\016\000\000\000", 4, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: partial hdr, partial header, + // body ("hello, world!\n"), partial footer, partial footer + {"\037\213\010\000\216", 5, 0}, + {"\176\356\075\002\003", 5, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266", 4, 1}, + {"\016\000\000\000", 4, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: partial hdr, partial header, + // body ("hello, world!\n") with partial footer, + // partial footer, partial footer + {"\037\213\010\000\216", 5, 0}, + {"\176\356\075\002\003", 5, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000" + // start of footer here. + "\300\337", + 18, 0}, + {"\061\266\016\000", 4, 1}, + {"\000\000", 2, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }}; + + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + for (int k = 0; k < 6; ++k) { + // k: < 3 with ZLib::should_be_flexible_with_gzip_footer_ true + // >= 3 with ZLib::should_be_flexible_with_gzip_footer_ false + // 0/3: no footer (partial footer for the last testing case) + // 1/4: normal footer + // 2/5: extra byte after footer + const int level = k % 3; + ZLib::set_should_be_flexible_with_gzip_footer(k < 3); + for (int j = 0; j < ABSL_ARRAYSIZE(comp_chunks); ++j) { + int bytes_uncompressed = 0; + zlib->Reset(); + int err = Z_OK; + for (int i = 0; comp_chunks[j][i].len != 0; ++i) { + if (comp_chunks[j][i].level <= level) { + uLongf uncomplen2 = uncompbuf2.size() - bytes_uncompressed; + err = zlib->UncompressChunk( + (Bytef*)&uncompbuf2[0] + bytes_uncompressed, &uncomplen2, + (const Bytef*)comp_chunks[j][i].s, comp_chunks[j][i].len); + if (err != Z_OK) { + LOG(INFO) << "err = " << err << " comp_chunks[" << j << "][" << i + << "] failed."; + break; + } else { + bytes_uncompressed += uncomplen2; + } + } + } + // With ZLib::should_be_flexible_with_gzip_footer_ being false, the no or + // partial footer (k == 3) and extra byte after footer (k == 5) cases + // should not work. With ZLib::should_be_flexible_with_gzip_footer_ being + // true, all cases should work. + if (k == 3 || k == 5) { + ASSERT_TRUE(err != Z_OK || !zlib->UncompressChunkDone()); + } else { + ASSERT_TRUE(zlib->UncompressChunkDone()); + LOG(INFO) << "Got " << bytes_uncompressed << " bytes: " + << absl::string_view(uncompbuf2.data(), bytes_uncompressed); + EXPECT_EQ(sizeof("hello, world!\n") - 1, bytes_uncompressed); + EXPECT_EQ(0, strncmp(uncompbuf2.data(), "hello, world!\n", + bytes_uncompressed)) + << "Uncompression mismatch, expected 'hello, world!\\n', " + << "got '" + << absl::string_view(uncompbuf2.data(), bytes_uncompressed) << "'"; + } + } + } +} + +// Take some test inputs and pass them to zlib, fragmenting the input +// in many different random ways. +void TestRandomGzipHeaderUncompress(ZLib* zlib) { + ACMRandom rnd(ACMRandom::DeprecatedDefaultSeed()); + + struct TestCase { + const char* str; + int len; // total length of the string + }; + TestCase tests[] = { + { + // header, body ("hello, world!\n"), footer + "\037\213\010\000\216\176\356\075\002\003" + "\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000" + "\300\337\061\266\016\000\000\000", + 34, + }, + }; + + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + // Test all the headers test cases. + for (int i = 0; i < ABSL_ARRAYSIZE(tests); ++i) { + // Test many random ways they might be fragmented. + for (int j = 0; j < 5 * 1000; ++j) { + // Get the test case set up. + const char* p = tests[i].str; + int bytes_left = tests[i].len; + int bytes_read = 0; + int bytes_uncompressed = 0; + zlib->Reset(); + + // Pick some random places to fragment the headers. + const int num_fragments = rnd.Uniform(bytes_left); + std::vector fragment_starts; + for (int frag_num = 0; frag_num < num_fragments; ++frag_num) { + fragment_starts.push_back(rnd.Uniform(bytes_left)); + } + std::sort(fragment_starts.begin(), fragment_starts.end()); + + VLOG(1) << "====="; + + // Go through several fragments and pass them in for parsing. + int frag_num = 0; + while (bytes_left > 0) { + const int fragment_len = (frag_num < num_fragments) + ? (fragment_starts[frag_num] - bytes_read) + : (tests[i].len - bytes_read); + ASSERT_GE(fragment_len, 0); + if (fragment_len != 0) { // zlib doesn't like 0-length buffers + VLOG(1) << absl::StrFormat( + "Passing %2d bytes at %2d..%2d: %s", fragment_len, bytes_read, + bytes_read + fragment_len, + absl::CEscape(std::string(p, fragment_len))); + + uLongf uncomplen2 = uncompbuf2.size() - bytes_uncompressed; + int err = + zlib->UncompressChunk((Bytef*)&uncompbuf2[0] + bytes_uncompressed, + &uncomplen2, (const Bytef*)p, fragment_len); + ASSERT_EQ(err, Z_OK); + bytes_uncompressed += uncomplen2; + bytes_read += fragment_len; + bytes_left -= fragment_len; + ASSERT_GE(bytes_left, 0); + p += fragment_len; + } + frag_num++; + } // while bytes left to uncompress + + ASSERT_TRUE(zlib->UncompressChunkDone()); + VLOG(1) << "Got " << bytes_uncompressed << " bytes: " + << absl::string_view(uncompbuf2.data(), bytes_uncompressed); + EXPECT_EQ(sizeof("hello, world!\n") - 1, bytes_uncompressed); + EXPECT_EQ( + 0, strncmp(uncompbuf2.data(), "hello, world!\n", bytes_uncompressed)) + << "Uncompression mismatch, expected 'hello, world!\\n', " + << "got '" << absl::string_view(uncompbuf2.data(), bytes_uncompressed) + << "'"; + } // for many fragmentations + } // for all test case headers +} + +// Make sure we give the proper error codes when inputs aren't quite kosher +void TestErrors(ZLib* zlib, const std::string& uncompbuf_str) { + const char* uncompbuf = uncompbuf_str.data(); + const uLongf uncomplen = uncompbuf_str.size(); + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + int err; + + uLongf complen = 23; // don't give it enough space to compress + err = zlib->Compress((Bytef*)compbuf.data(), &complen, (Bytef*)uncompbuf, + uncomplen); + EXPECT_EQ(Z_BUF_ERROR, err); + + // OK, now sucessfully compress + complen = compbuf.size(); + err = zlib->Compress((Bytef*)compbuf.data(), &complen, (Bytef*)uncompbuf, + uncomplen); + EXPECT_EQ(Z_OK, err) << " " << uncomplen << " bytes down to " << complen + << " bytes."; + + uLongf uncomplen2 = 100; // not enough space to uncompress + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_BUF_ERROR, err); + + // Here we check what happens when we don't try to uncompress enough bytes + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), 23); + EXPECT_EQ(Z_BUF_ERROR, err); + + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), 23); + EXPECT_EQ(Z_OK, err); // it's ok if a single chunk is too small + if (err == Z_OK) { + EXPECT_FALSE(zlib->UncompressChunkDone()) + << "UncompresDone() was happy with its 3 bytes of compressed data"; + } + + const int changepos = 0; + const char oldval = compbuf[changepos]; // corrupt the input + compbuf[changepos]++; + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_NE(Z_OK, err); + + compbuf[changepos] = oldval; + + // Make sure our memory-allocating uncompressor deals with problems gracefully + char* tmpbuf; + char tmp_compbuf[10] = "\255\255\255\255\255\255\255\255\255"; + uncomplen2 = FLAGS_zlib_max_size_uncompressed_data; + err = zlib->UncompressGzipAndAllocate( + (Bytef**)&tmpbuf, &uncomplen2, (Bytef*)tmp_compbuf, sizeof(tmp_compbuf)); + EXPECT_NE(Z_OK, err); + EXPECT_EQ(nullptr, tmpbuf); +} + +// Make sure that UncompressGzipAndAllocate returns a correct error +// when asked to uncompress data that isn't gzipped. +void TestBogusGunzipRequest(ZLib* zlib) { + const Bytef compbuf[] = "This is not compressed"; + const uLongf complen = sizeof(compbuf); + Bytef* uncompbuf; + uLongf uncomplen = 0; + int err = + zlib->UncompressGzipAndAllocate(&uncompbuf, &uncomplen, compbuf, complen); + EXPECT_EQ(Z_DATA_ERROR, err); +} + +void TestGzip(ZLib* zlib, const std::string& uncompbuf_str) { + const char* uncompbuf = uncompbuf_str.data(); + const uLongf uncomplen = uncompbuf_str.size(); + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + + uLongf complen = compbuf.size(); + int err = zlib->Compress((Bytef*)compbuf.data(), &complen, (Bytef*)uncompbuf, + uncomplen); + EXPECT_EQ(Z_OK, err) << " " << uncomplen << " bytes down to " << complen + << " bytes."; + + uLongf uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncomplen, uncomplen2) << "Uncompression mismatch!"; + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen)) + << "Uncompression mismatch!"; + + // Also try the auto-allocate uncompressor + char* tmpbuf; + err = zlib->UncompressGzipAndAllocate((Bytef**)&tmpbuf, &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncomplen, uncomplen2) << "Uncompression mismatch!"; + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen)) + << "Uncompression mismatch!"; + if (tmpbuf) free(tmpbuf); +} + +void TestChunkedGzip(ZLib* zlib, const std::string& uncompbuf_str, + int num_chunks) { + const char* uncompbuf = uncompbuf_str.data(); + const uLongf uncomplen = uncompbuf_str.size(); + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + CHECK_GT(num_chunks, 2); + + // uncompbuf2 is larger than uncompbuf to test for decoding too much and for + // historical reasons. + // + // Note that it is possible to receive num_chunks+1 total + // chunks, due to rounding error. + const int chunklen = uncomplen / num_chunks; + int chunknum, i, err; + int cum_len[num_chunks + 10]; // cumulative compressed length + cum_len[0] = 0; + for (chunknum = 0, i = 0; i < uncomplen; i += chunklen, chunknum++) { + uLongf complen = compbuf.size() - cum_len[chunknum]; + // Make sure the last chunk gets the correct chunksize. + int chunksize = (uncomplen - i) < chunklen ? (uncomplen - i) : chunklen; + err = zlib->CompressChunk((Bytef*)compbuf.data() + cum_len[chunknum], + &complen, (Bytef*)uncompbuf + i, chunksize); + ASSERT_EQ(Z_OK, err) << " " << uncomplen << " bytes down to " << complen + << " bytes."; + cum_len[chunknum + 1] = cum_len[chunknum] + complen; + } + uLongf complen = compbuf.size() - cum_len[chunknum]; + err = zlib->CompressChunkDone((Bytef*)compbuf.data() + cum_len[chunknum], + &complen); + EXPECT_EQ(Z_OK, err); + cum_len[chunknum + 1] = cum_len[chunknum] + complen; + + for (chunknum = 0, i = 0; i < uncomplen; i += chunklen, chunknum++) { + uLongf uncomplen2 = uncomplen - i; + // Make sure the last chunk gets the correct chunksize. + int expected = uncomplen2 < chunklen ? uncomplen2 : chunklen; + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0] + i, &uncomplen2, + (Bytef*)compbuf.data() + cum_len[chunknum], + cum_len[chunknum + 1] - cum_len[chunknum]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(expected, uncomplen2) + << "Uncompress size is " << uncomplen2 << ", not " << expected; + } + // There should be no further uncompressed bytes, after uncomplen bytes. + uLongf uncomplen2 = uncompbuf2.size() - uncomplen; + EXPECT_NE(0, uncomplen2); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0] + uncomplen, &uncomplen2, + (Bytef*)compbuf.data() + cum_len[chunknum], + cum_len[chunknum + 1] - cum_len[chunknum]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(0, uncomplen2); + EXPECT_TRUE(zlib->UncompressChunkDone()); + + // Those uncomplen bytes should match. + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen)) + << "Uncompression mismatch!"; + + // Now test to make sure resetting works properly + // (1) First, uncompress the first chunk and make sure it's ok + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), cum_len[1]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(chunklen, uncomplen2) << "Uncompression mismatch!"; + // The first uncomplen2 bytes should match, where uncomplen2 is the number of + // successfully uncompressed bytes by the most recent UncompressChunk call. + // The remaining (uncomplen - uncomplen2) bytes would still match if the + // uncompression guaranteed not to modify the buffer other than those first + // uncomplen2 bytes, but there is no such guarantee. + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // (2) Now, try the first chunk again and see that there's an error + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), cum_len[1]); + EXPECT_EQ(Z_DATA_ERROR, err); + + // (3) Now reset it and try again, and see that it's ok + zlib->Reset(); + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), cum_len[1]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(chunklen, uncomplen2) << "Uncompression mismatch!"; + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // (4) Make sure we can tackle output buffers that are too small + // with the *AtMost() interfaces. + uLong source_len = cum_len[2] - cum_len[1]; + CHECK_GT(source_len, 1); + uncomplen2 = source_len / 2; + err = zlib->UncompressAtMost((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)(compbuf.data() + cum_len[1]), + &source_len); + EXPECT_EQ(Z_BUF_ERROR, err); + + EXPECT_EQ(0, memcmp(uncompbuf + chunklen, uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + const int saveuncomplen2 = uncomplen2; + uncomplen2 = uncompbuf2.size() - uncomplen2; + // Uncompress the rest of the chunk. + err = zlib->UncompressAtMost( + (Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)(compbuf.data() + cum_len[2] - source_len), &source_len); + + EXPECT_EQ(Z_OK, err); + + EXPECT_EQ(0, memcmp(uncompbuf + chunklen + saveuncomplen2, uncompbuf2.data(), + uncomplen2)) + << "Uncompression mismatch!"; + + // (5) Finally, reset again so the rest of the tests can succeed. :-) + zlib->Reset(); +} + +void TestFooterBufferTooSmall(ZLib* zlib) { + uLongf footer_len = zlib->MinFooterSize() - 1; + ASSERT_EQ(9, footer_len); + Bytef footer_buffer[footer_len]; + int err = zlib->CompressChunkDone(footer_buffer, &footer_len); + ASSERT_EQ(Z_BUF_ERROR, err); + ASSERT_EQ(0, footer_len); +} + +// Helper routine for running a program and capturing its output +std::string RunCommand(const std::string& cmd) { + LOG(INFO) << "Running [" << cmd << "]"; + FILE* f = popen(cmd.c_str(), "r"); + CHECK(f != nullptr) << ": " << cmd << " failed"; + std::string result; + while (!feof(f) && !ferror(f)) { + char buf[1000]; + int n = fread(buf, 1, sizeof(buf), f); + CHECK(n >= 0); + result.append(buf, n); + } + CHECK(!ferror(f)); + pclose(f); + return result; +} + +// Helper routine to get uncompressed format of a string +std::string UncompressString(const std::string& input) { + Bytef* dest; + uLongf dest_len = FLAGS_zlib_max_size_uncompressed_data; + ZLib z; + z.SetGzipHeaderMode(); + int err = z.UncompressGzipAndAllocate(&dest, &dest_len, (Bytef*)input.data(), + input.size()); + CHECK_EQ(err, Z_OK); + std::string result((char*)dest, dest_len); + free(dest); + return result; +} + +class ZLibWrapperTest : public ::testing::TestWithParam { + protected: + // Returns the dictionary to use in our tests. If --dict is specified, the + // file pointed to by that flag is read in and used as the dictionary. + // Otherwise, a short default dictionary is used. + std::string GetDict() { + std::string dict; + const long kMaxDictLen = 32768; + + // Read in dictionary if specified, else use a default one. + if (!FLAGS_dict.empty()) { + CHECK(ReadFileToString(FLAGS_dict, &dict, kMaxDictLen)); + LOG(INFO) << "Read dictionary from " << FLAGS_dict << " (size " + << dict.size() << ")."; + } else { + dict = "this is a sample dictionary of the and or but not We URL"; + LOG(INFO) << "Using built-in dictionary (size " << dict.size() << ")."; + } + return dict; + } + + std::string ReadFileToTest(const std::string& filename) { + std::string uncompbuf; + LOG(INFO) << "Testing file: " << filename; + CHECK(ReadFileToString(filename, &uncompbuf, MAX_BUF_SIZE)); + return uncompbuf; + } +}; + +TEST(ZLibWrapperTest, HugeCompression) { + SlowTestLimiter limiter; + if (limiter.IsSlowBuild()) { + LOG(WARNING) << "Skipping test. Reason: " << limiter.reason(); + return; + } + + int lvl = FLAGS_min_comp_lvl; + + // Just big enough to trigger 32 bit overflow in MinCompressbufSize() + // calculation. + const uLong HUGE_DATA_SIZE = 0x81000000; + + // Construct an easily compressible huge buffer. + std::string uncompbuf(HUGE_DATA_SIZE, 'A'); + + LOG(INFO) << "Huge compression at level " << lvl; + ZLib zlib; + zlib.SetCompressionLevel(lvl); + TestCompression(&zlib, uncompbuf, nullptr); +} + +TEST_P(ZLibWrapperTest, Compression) { + const std::string dict = GetDict(); + const std::string uncompbuf = ReadFileToTest(GetParam()); + + for (int lvl = FLAGS_min_comp_lvl; lvl <= FLAGS_max_comp_lvl; lvl++) { + for (int no_header_mode = 0; no_header_mode <= 1; no_header_mode++) { + ZLib zlib; + zlib.SetCompressionLevel(lvl); + zlib.SetNoHeaderMode(no_header_mode); + + // TODO(gromer): Restructure the following code to minimize use of helper + // functions and LOG(INFO). + LOG(INFO) << "Level " << lvl << ", no_header_mode " << no_header_mode + << " (No dict)"; + TestCompression(&zlib, uncompbuf, " No dict"); + LOG(INFO) << "Level " << lvl << ", no_header_mode " << no_header_mode; + TestCompression(&zlib, uncompbuf, nullptr); + + // Try with a dictionary. For reasons I don't entirely understand, + // no_header_mode does not coexist with preloaded dictionaries. + if (!no_header_mode) { // try it with a dictionary + char dict_msg[64]; + snprintf(dict_msg, sizeof(dict_msg), " Dict %u", + static_cast(dict.size())); + zlib.SetDictionary(dict.data(), dict.size()); + LOG(INFO) << "Level " << lvl << " dict: " << dict_msg; + TestCompression(&zlib, uncompbuf, dict_msg); + LOG(INFO) << "Level " << lvl; + TestCompression(&zlib, uncompbuf, nullptr); + } + } + } +} + +// Make sure we deal correctly with a bug in old versions of zlibwrapper +TEST_P(ZLibWrapperTest, BuggyCompression) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + LOG(INFO) << "workaround for old zlibwrapper bug"; + TestBuggyCompression(&zlib, uncompbuf); + + // Try compressing again using the same ZLib + LOG(INFO) << "workaround for old zlibwrapper bug: same ZLib"; + TestBuggyCompression(&zlib, uncompbuf); +} + +// Test other problems +TEST_P(ZLibWrapperTest, OtherErrors) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + zlib.SetNoHeaderMode(false); + LOG(INFO) + << "Testing robustness against various errors: no_header_mode = false"; + TestErrors(&zlib, uncompbuf); + + zlib.SetNoHeaderMode(true); + LOG(INFO) + << "Testing robustness against various errors: no_header_mode = true"; + TestErrors(&zlib, uncompbuf); + + zlib.SetGzipHeaderMode(); + LOG(INFO) << "Testing robustness against various errors: gzip_header_mode"; + TestErrors(&zlib, uncompbuf); + + LOG(INFO) + << "Testing robustness against various errors: bogus gunzip request"; + TestBogusGunzipRequest(&zlib); +} + +// Make sure that (Un-)compress returns a correct error when asked to +// (un-)compress into a buffer bigger than 2^32 bytes. +// Running this with blaze --config=msan exposed the bug underlying +// http://b/25308089. +TEST_P(ZLibWrapperTest, TestBuffersTooBigFails) { + uLongf valid_len = 100; + uLongf invalid_len = 5000000000; // Bigger than 32 bit supported by zlib. + const Bytef* data = reinterpret_cast("test"); + uLongf data_len = 5; + // This test is not reusing the Zlib object so msan can determine + // when it's used uninitialized. + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Compress(nullptr, &invalid_len, data, data_len)); + } + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Compress(nullptr, &valid_len, nullptr, invalid_len)); + } + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Uncompress(nullptr, &invalid_len, data, data_len)); + } + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Uncompress(nullptr, &valid_len, nullptr, invalid_len)); + } +} + +// Make sure we deal correctly with compressed headers chunked weirdly +TEST_P(ZLibWrapperTest, UncompressChunked) { + { + ZLib zlib; + zlib.SetGzipHeaderMode(); + LOG(INFO) << "Uncompressing gzip headers"; + TestGzipHeaderUncompress(&zlib); + } + { + ZLib zlib; + zlib.SetGzipHeaderMode(); + LOG(INFO) << "Uncompressing randomly-fragmented gzip headers"; + TestRandomGzipHeaderUncompress(&zlib); + } +} + +// Now test gzip compression. +TEST_P(ZLibWrapperTest, GzipCompression) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + zlib.SetGzipHeaderMode(); + LOG(INFO) << "gzip compression"; + TestGzip(&zlib, uncompbuf); + + // Try compressing again using the same ZLib + LOG(INFO) << "gzip compression: same ZLib"; + TestGzip(&zlib, uncompbuf); +} + +// Now test chunked compression. +TEST_P(ZLibWrapperTest, ChunkedCompression) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + zlib.SetGzipHeaderMode(); + LOG(INFO) << "chunked gzip compression"; + // At this point is the minimum between MAX_BUF_SIZE (1048500) + // and the size of the last input file processed. With a larger file, + // uncompen is a multiple of both 10, 20 and 100. + // Using 21 chunks cause the last chunk to be smaller than the others. + TestChunkedGzip(&zlib, uncompbuf, 21); + + // Try compressing again using the same ZLib + LOG(INFO) << "chunked gzip compression: same ZLib"; + TestChunkedGzip(&zlib, uncompbuf, 20); + + // In theory we can mix and match the type of compression we do + LOG(INFO) << "chunked gzip compression: different compression type"; + TestGzip(&zlib, uncompbuf); + LOG(INFO) << "chunked gzip compression: original compression type"; + TestChunkedGzip(&zlib, uncompbuf, 100); + + // Test writing final chunk and footer into buffer that's too small. + LOG(INFO) << "chunked gzip compression: buffer too small"; + TestFooterBufferTooSmall(&zlib); + + LOG(INFO) << "chunked gzip compression: not chunked"; + TestGzip(&zlib, uncompbuf); +} + +// Simple helper to force specialization of strings::Split. +std::vector GetFilesToProcess() { + std::string files_to_process = + FLAGS_files_to_process.empty() + ? absl::StrCat(FLAGS_test_srcdir, "/google3/util/gtl/testdata/words") + : FLAGS_files_to_process; + return absl::StrSplit(files_to_process, ",", absl::SkipWhitespace()); +} + +INSTANTIATE_TEST_SUITE_P(AllTests, ZLibWrapperTest, + ::testing::ValuesIn(GetFilesToProcess())); + +TEST(ZLibWrapperStandaloneTest, GzipCompatibility) { + LOG(INFO) << "Testing compatibility with gzip output"; + const std::string input = "hello world"; + std::string gzip_output = + RunCommand(absl::StrCat("echo ", input, " | gzip -c")); + ASSERT_EQ(absl::StrCat(input, "\n"), UncompressString(gzip_output)); +} + +/* + The Gzip footer contains the lower four bytes of the uncompressed length. + Previously IsGzipFooterValid() compared the value from the footer with the + entire length, not the lower four bytes of the length. + + To test this, compress a 4 GB file a chunk at a time. + */ +TEST(ZLibWrapperStandaloneTest, DecompressHugeFileWithFooter) { + SlowTestLimiter limiter; + + ZLib compressor; + // We specifically want to test that we validate the footer correctly. + compressor.SetGzipHeaderMode(); + + ZLib decompressor; + decompressor.SetGzipHeaderMode(); + + const int64 uncompressed_size = 1LL << 32; // too big for a 4-byte int + int64 uncompressed_bytes_sent = 0; + + const int64 chunk_size = 10 * 1024 * 1024; + std::string inbuf(chunk_size, '\0'); // The input data + std::string compbuf(chunk_size, '\0'); // The compressed data + std::string outbuf(chunk_size, '\0'); // The output data + while (uncompressed_bytes_sent < uncompressed_size) { + if (limiter.DeadlineExceeded()) { + LOG(WARNING) << "Ending test early, after " << uncompressed_bytes_sent + << " of " << uncompressed_size + << " bytes. Reason: " << limiter.reason(); + return; + } + + // Compress a chunk. + uLongf complen = chunk_size; + ASSERT_EQ(Z_OK, + compressor.CompressChunk((Bytef*)compbuf.data(), &complen, + (Bytef*)inbuf.data(), inbuf.size())); + + // Uncompress a chunk. + uLongf outlen = chunk_size; + ASSERT_EQ(Z_OK, + decompressor.UncompressChunk((Bytef*)outbuf.data(), &outlen, + (Bytef*)compbuf.data(), complen)); + + ASSERT_EQ(outlen, inbuf.size()); + uncompressed_bytes_sent += inbuf.size(); + } + + // Write the footer chunk. + uLongf complen = chunk_size; + ASSERT_EQ(Z_OK, + compressor.CompressChunkDone((Bytef*)compbuf.data(), &complen)); + + // Read the footer chunk. + uLongf outlen = chunk_size; + ASSERT_EQ(Z_OK, + decompressor.UncompressChunk((Bytef*)outbuf.data(), &outlen, + (Bytef*)compbuf.data(), complen)); + + // This will fail if we validate the footer incorrectly. + ASSERT_TRUE(decompressor.UncompressChunkDone()); +} + +/* + Try to reproduce a bug in deflate, by repeatedly allocating compressors + and compressing a particular block of data (a Google News homepage that + I extracted from the core file of a crashed NFE). + + To see the bug you'll need to run an optimized build of the unittest (so + that malloc doesn't add headers) and remove the workaround from deflate.c + (see the comment in deflateInit2_ for details). + + The full story: + + The inner loop of deflate is in the function longest_match, comparing two + byte strings to find the length of the common prefix up to a maximum length + of 258. The string being examined is in a 64K byte buffer that zlib + allocates internally (called "window"); the data to be compressed is streamed + into it. The code that calls longest_match (deflate_slow) ensures that there + are always at least MIN_LOOKAHEAD (=262) bytes in the window beyond the start + of the string being examined. + + For performance longest_match has been rewritten in assembler (match.S), and + the inner loop compares 8 bytes at a time. The loop is written to always + examine 264 bytes, starting within a few bytes of the start of the string + (depending on alignment). If you start with a string of length 263 right at + the end of the buffer, you end up looking at a byte or two beyond the end of + the buffer. It doesn't matter whether those bytes match or not, since the + match length will get maxed against 258 anyway, but if you're unlucky and + the page after the buffer isn't mapped, you'll die. + + This never happened to GWS because it doesn't generate pages that are 64K + long. It doesn't happen to anyone outside google because everyone else's + malloc, when asked to alloc a 64K block, will actually allocate an extra + page to allow for the headers. But the NFE, a google front-end that + routinely generates 120K result pages, hit this bug about 20 times a day. +*/ +TEST(ZLibWrapperStandalone, ReadPastEndOfWindow) { + SlowTestLimiter limiter; + + std::string fname = FLAGS_read_past_window_data_file; + if (fname.empty()) { + fname = FLAGS_test_srcdir + + "/google3/third_party/zlib/testdata/read_past_window.data"; + } + std::string uncompbuf; + ASSERT_TRUE(ReadFileToString(fname, &uncompbuf, MAX_BUF_SIZE)); + const uLongf uncomplen = uncompbuf.size(); + ASSERT_TRUE(uncomplen >= 0x10000) << "not enough test data in " << fname; + + std::vector> used_zlibs; + unsigned long comprlen = ZLib::MinCompressbufSize(uncomplen); + std::string compr(comprlen, '\0'); + + for (int i = 0; i < FLAGS_read_past_window_iterations; ++i) { + if (limiter.DeadlineExceeded()) { + LOG(WARNING) << "Ending test after only " << i + << " of --read_past_window_iteratons=" + << FLAGS_read_past_window_iterations + << " iterations. Reason: " << limiter.reason(); + break; + } + + ZLib* zlib = new ZLib; + zlib->SetGzipHeaderMode(); + int rc = zlib->Compress((Bytef*)&compr[0], &comprlen, + (Bytef*)uncompbuf.data(), uncomplen); + ASSERT_EQ(rc, Z_OK); + used_zlibs.emplace_back(zlib); + } + + // if we haven't segfaulted by now, we pass + LOG(INFO) << "passed read-past-end-of-window test"; +} + +} // namespace +} // namespace firebase diff --git a/app/src/fake/FIRApp.h b/app/src/fake/FIRApp.h new file mode 100644 index 0000000000..70d81b1219 --- /dev/null +++ b/app/src/fake/FIRApp.h @@ -0,0 +1,58 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import + +extern "C" { + +// Test method to create an application using the specified name and default +// options. +void FIRAppCreateUsingDefaultOptions(const char* name); +// Test method to clear all app instances. +void FIRAppResetApps(); + +} + +@class FIROptions; + +typedef void (^FIRAppVoidBoolCallback)(BOOL success); + +/** + * A fake Firebase App class for unit-testing. + */ +@interface FIRApp : NSObject + +// Test method to clear all FIRApp instances. ++ (void)resetApps; + ++ (void)configure; + ++ (void)configureWithOptions:(FIROptions *)options; + ++ (void)configureWithName:(NSString *)name options:(FIROptions *)options; + ++ (FIRApp *)defaultApp; + ++ (FIRApp *)appNamed:(NSString *)name; + +- (void)deleteApp:(FIRAppVoidBoolCallback)completion; + +@property(nonatomic, copy, readonly) NSString *name; + +@property(nonatomic, copy, readonly) FIROptions *options; + +@property(nonatomic, readwrite, getter=isDataCollectionDefaultEnabled) + BOOL dataCollectionDefaultEnabled; + +@end diff --git a/app/src/fake/FIRApp.mm b/app/src/fake/FIRApp.mm new file mode 100644 index 0000000000..8657edf9d0 --- /dev/null +++ b/app/src/fake/FIRApp.mm @@ -0,0 +1,106 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "app/src/fake/FIRApp.h" + +#import "app/src/fake/FIROptions.h" + +static NSString *kFIRDefaultAppName = @"__FIRAPP_DEFAULT"; + +@implementation FIRApp + +@synthesize options = _options; +BOOL _dataCollectionEnabled; + +static NSMutableDictionary *sAllApps; + +- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options { + self = [super init]; + if (self) { + _name = [name copy]; + _options = [options copy]; + _dataCollectionEnabled = YES; + } + return self; +} + ++ (void)resetApps { + if (sAllApps) [sAllApps removeAllObjects]; +} + ++ (void)configure { + return [FIRApp configureWithOptions:[FIROptions defaultOptions]]; +} + ++ (void)configureWithOptions:(FIROptions *)options { + return [FIRApp configureWithName:kFIRDefaultAppName options:options]; +} + ++ (void)configureWithName:(NSString *)name options:(FIROptions *)options { + FIRApp *app = [[FIRApp alloc] initInstanceWithName:name options:options]; + if (!sAllApps) sAllApps = [[NSMutableDictionary alloc] init]; + sAllApps[app.name] = app; +} + ++ (FIRApp *)defaultApp { + return sAllApps ? sAllApps[kFIRDefaultAppName] : nil; +} + ++ (FIRApp *)appNamed:(NSString *)name { + return sAllApps ? sAllApps[name] : nil; +} + +- (void)deleteApp:(FIRAppVoidBoolCallback)completion { + if (sAllApps) { + [sAllApps removeObjectForKey:self.name]; + } + completion(TRUE); +} + +- (void)setDataCollectionDefaultEnabled:(BOOL)dataCollectionDefaultEnabled { + _dataCollectionEnabled = dataCollectionDefaultEnabled; +} + +- (BOOL)isDataCollectionDefaultEnabled { + return _dataCollectionEnabled; +} + +static NSMutableDictionary* sRegisteredLibraries = [[NSMutableDictionary alloc] init]; + ++ (void)registerLibrary:(nonnull NSString *)library withVersion:(nonnull NSString *)version { + if (sRegisteredLibraries.count == 0) sRegisteredLibraries[@"fire-ios"] = @"1.2.3"; + sRegisteredLibraries[library] = version; +} + ++ (NSString *)firebaseUserAgent { + NSMutableArray *libraries = + [[NSMutableArray alloc] initWithCapacity:sRegisteredLibraries.count]; + for (NSString *libraryName in sRegisteredLibraries) { + [libraries + addObject:[NSString stringWithFormat:@"%@/%@", libraryName, + sRegisteredLibraries[libraryName]]]; + } + [libraries sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + return [libraries componentsJoinedByString:@" "]; +} + +@end + +void FIRAppCreateUsingDefaultOptions(const char* name) { + [FIRApp configureWithName:@(name) options:[FIROptions defaultOptions]]; +} + +void FIRAppResetApps() { + [FIRApp resetApps]; +} diff --git a/app/src/fake/FIRConfiguration.h b/app/src/fake/FIRConfiguration.h new file mode 100644 index 0000000000..2b8e678ceb --- /dev/null +++ b/app/src/fake/FIRConfiguration.h @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRLoggerLevel.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This interface provides global level properties that the developer can tweak. + */ +NS_SWIFT_NAME(FirebaseConfiguration) +@interface FIRConfiguration : NSObject + +/** Returns the shared configuration object. */ +@property(class, nonatomic, readonly) FIRConfiguration *sharedInstance NS_SWIFT_NAME(shared); + +/** + * Sets the logging level for internal Firebase logging. Firebase will only log messages + * that are logged at or below loggerLevel. The messages are logged both to the Xcode + * console and to the device's log. Note that if an app is running from AppStore, it will + * never log above FIRLoggerLevelNotice even if loggerLevel is set to a higher (more verbose) + * setting. + * + * @param loggerLevel The maximum logging level. The default level is set to FIRLoggerLevelNotice. + */ +- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/app/src/fake/FIRConfiguration.m b/app/src/fake/FIRConfiguration.m new file mode 100644 index 0000000000..49cc8a17ed --- /dev/null +++ b/app/src/fake/FIRConfiguration.m @@ -0,0 +1,34 @@ +// Copyright 2017 Google +// +// 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. + +#import "FIRConfiguration.h" + +@implementation FIRConfiguration + ++ (instancetype)sharedInstance { + static FIRConfiguration *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FIRConfiguration alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + return [super init]; +} + +- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel {} + +@end diff --git a/app/src/fake/FIRLogger.h b/app/src/fake/FIRLogger.h new file mode 100644 index 0000000000..de50235616 --- /dev/null +++ b/app/src/fake/FIRLogger.h @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#import + +#import "FIRLoggerLevel.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Changes the default logging level of FIRLoggerLevelNotice to a user-specified level. + * The default level cannot be set above FIRLoggerLevelNotice if the app is running from App Store. + * (required) log level (one of the FIRLoggerLevel enum values). + */ +void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel); + +#ifdef __cplusplus +} +#endif // __cplusplus diff --git a/app/src/fake/FIRLoggerLevel.h b/app/src/fake/FIRLoggerLevel.h new file mode 100644 index 0000000000..dca3aa0b01 --- /dev/null +++ b/app/src/fake/FIRLoggerLevel.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +// Note that importing GULLoggerLevel.h will lead to a non-modular header +// import error. + +/** + * The log levels used by internal logging. + */ +typedef NS_ENUM(NSInteger, FIRLoggerLevel) { + /** Error level, matches ASL_LEVEL_ERR. */ + FIRLoggerLevelError = 3, + /** Warning level, matches ASL_LEVEL_WARNING. */ + FIRLoggerLevelWarning = 4, + /** Notice level, matches ASL_LEVEL_NOTICE. */ + FIRLoggerLevelNotice = 5, + /** Info level, matches ASL_LEVEL_INFO. */ + FIRLoggerLevelInfo = 6, + /** Debug level, matches ASL_LEVEL_DEBUG. */ + FIRLoggerLevelDebug = 7, + /** Minimum log level. */ + FIRLoggerLevelMin = FIRLoggerLevelError, + /** Maximum log level. */ + FIRLoggerLevelMax = FIRLoggerLevelDebug +} NS_SWIFT_NAME(FirebaseLoggerLevel); diff --git a/app/src/fake/FIROptions.h b/app/src/fake/FIROptions.h new file mode 100644 index 0000000000..b9a07cb214 --- /dev/null +++ b/app/src/fake/FIROptions.h @@ -0,0 +1,51 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import + +/** + * A fake Firebase Option class for unit-testing. + */ +@interface FIROptions : NSObject + +@property(nonatomic, copy) NSString *APIKey; + +@property(nonatomic, copy) NSString *bundleID; + +@property(nonatomic, copy) NSString *clientID; + +@property(nonatomic, copy) NSString *trackingID; + +@property(nonatomic, copy) NSString *GCMSenderID; + +@property(nonatomic, copy) NSString *projectID; + +@property(nonatomic, copy) NSString *androidClientID; + +@property(nonatomic, copy) NSString *googleAppID; + +@property(nonatomic, copy) NSString *databaseURL; + +@property(nonatomic, copy) NSString *deepLinkURLScheme; + +@property(nonatomic, copy) NSString *storageBucket; + +@property(nonatomic, copy) NSString *appGroupID; + ++ (FIROptions *)defaultOptions; + +- (instancetype)initWithGoogleAppID:(NSString *)googleAppID + GCMSenderID:(NSString *)GCMSenderID; + +@end diff --git a/app/src/fake/FIROptions.mm b/app/src/fake/FIROptions.mm new file mode 100644 index 0000000000..c3042d2829 --- /dev/null +++ b/app/src/fake/FIROptions.mm @@ -0,0 +1,66 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "app/src/fake/FIROptions.h" + +@implementation FIROptions + ++ (FIROptions *)defaultOptions { + FIROptions* options = + [[FIROptions alloc] initWithGoogleAppID:@"fake google app id from resource" + GCMSenderID:@"fake messaging sender id from resource"]; + options.APIKey = @"fake api key from resource"; + options.bundleID = @"fake bundle ID from resource"; + options.clientID = @"fake client id from resource"; + options.trackingID = @"fake ga tracking id from resource"; + options.projectID = @"fake project id from resource"; + options.androidClientID = @"fake android client id from resource"; + options.googleAppID = @"fake app id from resource"; + options.databaseURL = @"fake database url from resource"; + options.deepLinkURLScheme = @"fake deep link url scheme from resource"; + options.storageBucket = @"fake storage bucket from resource"; + return options; +} + +- (instancetype)initWithGoogleAppID:(NSString *)googleAppID + GCMSenderID:(NSString *)GCMSenderID { + self = [super init]; + if (self) { + _googleAppID = googleAppID; + _GCMSenderID = GCMSenderID; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + FIROptions *newOptions = [[[self class] allocWithZone:zone] init]; + if (newOptions) { + newOptions.googleAppID = self.googleAppID; + newOptions.GCMSenderID = self.GCMSenderID; + newOptions.APIKey = self.APIKey; + newOptions.bundleID = self.bundleID; + newOptions.clientID = self.clientID; + newOptions.trackingID = self.trackingID; + newOptions.projectID = self.projectID; + newOptions.androidClientID = self.androidClientID; + newOptions.googleAppID = self.googleAppID; + newOptions.databaseURL = self.databaseURL; + newOptions.deepLinkURLScheme = self.deepLinkURLScheme; + newOptions.storageBucket = self.storageBucket; + newOptions.appGroupID = self.appGroupID; + } + return newOptions; +} + +@end diff --git a/app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java b/app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java new file mode 100644 index 0000000000..9bdfa3ab52 --- /dev/null +++ b/app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.android.gms.common; + +import android.content.Context; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; + +/** Fake gms/common/GoogleApiAvailability.java for unit testing. */ +public final class GoogleApiAvailability { + + private static final GoogleApiAvailability INSTANCE = new GoogleApiAvailability(); + + public static GoogleApiAvailability getInstance() { + ConfigRow row = ConfigAndroid.get("GoogleApiAvailability.getInstance"); + if (row != null) { + // Right now we let it returns null and ignore whatever set. + return null; + } + + // Default behavior + return INSTANCE; + } + + public int isGooglePlayServicesAvailable(Context context) { + ConfigRow row = ConfigAndroid.get("GoogleApiAvailability.isGooglePlayServicesAvailable"); + if (row != null) { + return row.futureint().value(); + } + + // Default behavior + return 0; + } +} diff --git a/app/src_java/fake/com/google/android/gms/tasks/Task.java b/app/src_java/fake/com/google/android/gms/tasks/Task.java new file mode 100644 index 0000000000..04a3b4cbe9 --- /dev/null +++ b/app/src_java/fake/com/google/android/gms/tasks/Task.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.android.gms.tasks; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeListener; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import com.google.firebase.testing.cppsdk.TickerObserver; +import java.util.Vector; + +/** + * Fake Task class that accepts instruction from {@link setEta}, {@link setResult} and {@link + * setException} and acts accordingly. + */ +public class Task implements TickerObserver { + private final Vector> mListenerQueue = new Vector<>(); + private long eta; + private TResult mResult; + private Exception mException; + + public TResult getResult() throws Exception { + if (mException != null) { + throw mException; + } + return mResult; + } + + @Override + public void elapse() { + if (isComplete() && !mListenerQueue.isEmpty()) { + for (FakeListener listener : mListenerQueue) { + if (mException == null) { + listener.onSuccess(mResult); + } else { + listener.onFailure(mException); + } + } + mListenerQueue.clear(); + } + } + + public boolean isComplete() { + return eta <= TickerAndroid.now(); + } + + public boolean isSuccessful() { + return isComplete() && mException == null; + } + + public void setEta(long eta) { + this.eta = eta; + } + + /** Set what result the task should return unless you also call {@link setException}. */ + public void setResult(TResult result) { + mResult = result; + } + + /** Set an exception the task should throw. */ + public void setException(Exception e) { + mException = e; + } + + /** + * To make writing fake less cumbersome, we use a single type of {@link FakeListener} to mimic all + * types of listeners. + */ + public Task addListener(FakeListener listener) { + mListenerQueue.add(listener); + elapse(); + return this; + } + + /** A helper function to get a task that returns immediately the specified result. */ + public static Task forResult(TResult result) { + Task task = new Task<>(); + task.setResult(result); + task.setEta(0L); + return task; + } + + /** A helper function to get a task from a {@link ConfigRow}. */ + public static Task forResult(String configKey, TResult result) { + ConfigRow row = ConfigAndroid.get(configKey); + if (row == null) { + // Default behavior when no config is set. + return forResult(result); + } + + Task task = new Task<>(); + if (row.futuregeneric().throwexception()) { + task.setException(new Exception(row.futuregeneric().exceptionmsg())); + } else { + task.setResult(result); + } + task.setEta(row.futuregeneric().ticker()); + return task; + } +} diff --git a/app/src_java/fake/com/google/firebase/FirebaseApp.java b/app/src_java/fake/com/google/firebase/FirebaseApp.java new file mode 100644 index 0000000000..da148ca1d6 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/FirebaseApp.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +import android.content.Context; +import java.util.HashMap; + +/** Fake //j/c/g/a/gmscore/integ/client/firebase_common/src/com/google/firebase/FirebaseApp.java */ +public final class FirebaseApp { + static final String DEFAULT_NAME = "[DEFAULT]"; + static final HashMap instances = new HashMap<>(); + + String name; + FirebaseOptions options; + + // Exposed to clear all FirebaseApp instances. This should be called between each test case. + public static void reset() { + instances.clear(); + } + + public static FirebaseApp initializeApp(Context context, FirebaseOptions options) { + return initializeApp(context, options, DEFAULT_NAME); + } + + public static FirebaseApp initializeApp(Context context, FirebaseOptions options, String name) { + if (!instances.containsKey(name)) { + instances.put(name, new FirebaseApp(name, options)); + } + return getInstance(name); + } + + public static FirebaseApp getInstance() { + return getInstance(DEFAULT_NAME); + } + + public static FirebaseApp getInstance(String name) { + FirebaseApp app = instances.get(name); + if (app == null) { + throw new IllegalStateException(String.format("FirebaseApp %s does not exist", name)); + } + return app; + } + + private FirebaseApp(String name, FirebaseOptions options) { + this.name = name; + this.options = options; + } + + public void delete() { + instances.remove(name); + } + + public FirebaseOptions getOptions() { + return options; + } + + public boolean isDataCollectionDefaultEnabled() { + return true; + } + + public void setDataCollectionDefaultEnabled(boolean enabled) {} +} diff --git a/app/src_java/fake/com/google/firebase/FirebaseException.java b/app/src_java/fake/com/google/firebase/FirebaseException.java new file mode 100644 index 0000000000..8d430cd072 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/FirebaseException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseException */ +public class FirebaseException extends Exception { + + public FirebaseException(String message) { + super(message); + } +} diff --git a/app/src_java/fake/com/google/firebase/FirebaseOptions.java b/app/src_java/fake/com/google/firebase/FirebaseOptions.java new file mode 100644 index 0000000000..7559eee97f --- /dev/null +++ b/app/src_java/fake/com/google/firebase/FirebaseOptions.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +import android.content.Context; +import android.util.Log; + +/** + * Fake //j/c/g/a/gmscore/integ/client/firebase_common/src/com/google/firebase/FirebaseOptions.java + */ +public final class FirebaseOptions { + private static final String LOG_TAG = "FakeFirebaseOptions"; + + /** Fake Builder. */ + public static final class Builder { + private String apiKey; + private String applicationId; + private String databaseUrl; + private String gcmSenderId; + private String storageBucket; + private String projectId; + + public Builder setApiKey(String apiKey) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set api key " + apiKey); + } + this.apiKey = apiKey; + return this; + } + + public Builder setDatabaseUrl(String databaseUrl) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set database url " + databaseUrl); + } + this.databaseUrl = databaseUrl; + return this; + } + + public Builder setApplicationId(String applicationId) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set application id " + applicationId); + } + this.applicationId = applicationId; + return this; + } + + public Builder setGcmSenderId(String gcmSenderId) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set gcm sender id " + gcmSenderId); + } + this.gcmSenderId = gcmSenderId; + return this; + } + + public Builder setStorageBucket(String storageBucket) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set storage bucket " + storageBucket); + } + this.storageBucket = storageBucket; + return this; + } + + public Builder setProjectId(String projectId) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set project id " + projectId); + } + this.projectId = projectId; + return this; + } + + public FirebaseOptions build() { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "built"); + } + return new FirebaseOptions(this); + } + } + + public Builder builder; + + private FirebaseOptions(Builder builder) { + this.builder = builder; + } + + public static FirebaseOptions fromResource(Context context) { + return new FirebaseOptions( + new Builder() + .setApiKey("fake api key from resource") + .setDatabaseUrl("fake database url from resource") + .setApplicationId("fake app id from resource") + .setGcmSenderId("fake messaging sender id from resource") + .setStorageBucket("fake storage bucket from resource") + .setProjectId("fake project id from resource")); + } + + public String getApiKey() { + return builder.apiKey; + } + + public String getApplicationId() { + return builder.applicationId; + } + + public String getDatabaseUrl() { + return builder.databaseUrl; + } + + public String getGcmSenderId() { + return builder.gcmSenderId; + } + + public String getStorageBucket() { + return builder.storageBucket; + } + + public String getProjectId() { + return builder.projectId; + } +} diff --git a/app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java b/app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java new file mode 100644 index 0000000000..9837ef421a --- /dev/null +++ b/app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.app.internal.cpp; + +import android.app.Activity; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** Runs a native C++ function on an alternate thread. */ +public class CppThreadDispatcher { + private static final ExecutorService executor = Executors.newSingleThreadExecutor( + Executors.defaultThreadFactory()); + + /** Runs a C++ function on the main thread using the executor. */ + public static void runOnMainThread(Activity activity, final CppThreadDispatcherContext context) { + Object unused = executor.submit(new Runnable() { + @Override + public void run() { + context.execute(); + } + }); + } + + /** Runs a C++ function on a new Java background thread. */ + public static void runOnBackgroundThread(final CppThreadDispatcherContext context) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + context.execute(); + } + }); + t.start(); + } +} diff --git a/app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java b/app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java new file mode 100644 index 0000000000..ee34b2e859 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.app.internal.cpp; + +import android.app.Activity; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FutureBoolResult; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import com.google.firebase.testing.cppsdk.TickerObserver; + +/** + * Fake //f/a/c/cpp/src_java/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java + */ +public final class GoogleApiAvailabilityHelper { + private static final int SUCCESS = 0; + + public static boolean makeGooglePlayServicesAvailable(Activity activity) { + final ConfigRow row = + ConfigAndroid.get("GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable"); + if (row != null) { + TickerAndroid.register( + new TickerObserver() { + @Override + public void elapse() { + if (TickerAndroid.now() == row.futureint().ticker()) { + int resultCode = row.futureint().value(); + onCompleteNative(resultCode, "result code is " + resultCode); + } + } + }); + return row.futurebool().value() == FutureBoolResult.True; + } + + // Default behavior + onCompleteNative(SUCCESS, "Google Play services are already available (fake)"); + return true; + } + + public static void stopCallbacks() {} + + private static native void onCompleteNative(int resultCode, String resultMessage); +} diff --git a/app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java b/app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java new file mode 100644 index 0000000000..0be47b678f --- /dev/null +++ b/app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.app.internal.cpp; + +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.FakeListener; + +/** + * Fake firebase/app/client/cpp/src_java/com/google/firebase/app/internal/cpp/JniResultCallback.java + */ +public class JniResultCallback { + private interface Callback { + public void register(); + + public void disconnect(); + }; + + private long callbackFn; + private long callbackData; + private Callback callbackHandler = null; + + public static final String LOG_TAG = "FakeFirebaseCb"; + + private class TaskCallback extends FakeListener implements Callback { + private Task task; + + public TaskCallback(Task task) { + this.task = task; + } + + @Override + public void onSuccess(T result) { + if (task != null) { + onCompletion(result, true, false, null); + } + disconnect(); + } + + @Override + public void onFailure(Exception exception) { + if (task != null) { + onCompletion(exception, false, false, exception.getMessage()); + } + disconnect(); + } + + @Override + public void register() { + task.addListener(this); + } + + @Override + public void disconnect() { + task = null; + } + } + + @SuppressWarnings("unchecked") + public JniResultCallback(Task task, long callbackFn, long callbackData) { + Log.i(LOG_TAG, String.format("JniResultCallback: Fn %x, Data %x", callbackFn, callbackData)); + this.callbackFn = callbackFn; + this.callbackData = callbackData; + callbackHandler = new TaskCallback<>(task); + callbackHandler.register(); + } + + public void cancel() { + Log.i(LOG_TAG, "canceled"); + onCompletion(null, false, true, "cancelled (fake)"); + } + + private void onCompletion( + Object result, boolean success, boolean cancelled, String statusMessage) { + if (callbackHandler != null) { + nativeOnResult( + result, success, cancelled, statusMessage, callbackFn, callbackData); + callbackHandler.disconnect(); + callbackHandler = null; + } + } + + private native void nativeOnResult( + Object result, + boolean success, + boolean cancelled, + String statusString, + long callbackFn, + long callbackData); +} diff --git a/app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java b/app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java new file mode 100644 index 0000000000..b5cf72cd61 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +package com.google.firebase.platforminfo; + +import java.util.HashSet; +import java.util.Set; + +/** + * Fake + * //j/c/g/a/gmscore/integ/client/firebase_common/src/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java + */ +public final class GlobalLibraryVersionRegistrar { + public static GlobalLibraryVersionRegistrar getInstance() { + return new GlobalLibraryVersionRegistrar(); + } + + public void registerVersion(String library, String version) {} + + public Set getRegisteredVersions() { + return new HashSet<>(); + } +} diff --git a/app/tests/CMakeLists.txt b/app/tests/CMakeLists.txt new file mode 100644 index 0000000000..07bd6dea1b --- /dev/null +++ b/app/tests/CMakeLists.txt @@ -0,0 +1,410 @@ +# Copyright 2019 Google LLC +# +# 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. + +# Set up a library for create an app suitable for testing. Needs an empty +# source file, as not all compilers handle header only libraries with CMake. + +file(WRITE ${CMAKE_BINARY_DIR}/empty.cc) +add_library(firebase_app_for_testing + ${CMAKE_BINARY_DIR}/empty.cc + ${FIREBASE_SOURCE_DIR}/app/tests/include/firebase/app_for_testing.h +) + +target_include_directories(firebase_app_for_testing + PUBLIC + ${FIREBASE_SOURCE_DIR}/app/tests/include/firebase +) + +if (ANDROID) +elseif (IOS) + set(TEST_RUNNER_DIR "${FIREBASE_SOURCE_DIR}/app/src/tests/runner/ios") + add_executable(firebase_app_for_testing_ios MACOSX_BUNDLE + ${TEST_RUNNER_DIR}/FIRAppDelegate.m + ${TEST_RUNNER_DIR}/FIRAppDelegate.h + ${TEST_RUNNER_DIR}/FIRViewController.m + ${TEST_RUNNER_DIR}/FIRViewController.h + ${TEST_RUNNER_DIR}/main.m + ${FIREBASE_SOURCE_DIR}/app/tests/include/firebase/app_for_testing.h + ${FIREBASE_SOURCE_DIR}/app/src/fake/FIRApp.mm + ${FIREBASE_SOURCE_DIR}/app/src/fake/FIROptions.mm + ) + + target_include_directories(firebase_app_for_testing_ios + PUBLIC + ${FIREBASE_SOURCE_DIR}/app/src/fake + PRIVATE + ${FIREBASE_SOURCE_DIR} + ) + + target_link_libraries( + firebase_app_for_testing_ios + PRIVATE + "-framework UIKit" + "-framework Foundation" + ) + set_target_properties( + firebase_app_for_testing_ios PROPERTIES + MACOSX_BUNDLE_INFO_PLIST + ${TEST_RUNNER_DIR}/Info.plist + RESOURCE + ${TEST_RUNNER_DIR}/Info.plist + ) +else() + set(rest_mocks_SRCS + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/rest/transport_mock.h + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/rest/transport_mock.cc) + + add_library(firebase_rest_mocks STATIC + ${rest_mocks_SRCS}) + target_include_directories(firebase_rest_mocks + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_GEN_FILE_DIR} + ) + target_link_libraries(firebase_rest_mocks + PRIVATE + firebase_rest_lib + firebase_testing + ) +endif() + +firebase_cpp_cc_test_on_ios(firebase_app_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/app_test.cc + HOST + firebase_app_for_testing_ios + DEPENDS + firebase_app_for_testing + firebase_app + firebase_testing + flatbuffers + CUSTOM_FRAMEWORKS + UIKit +) + +firebase_cpp_cc_test(firebase_app_log_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/log_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test_on_ios(firebase_app_log_test + HOST + firebase_app_for_testing_ios + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/log_test.cc + DEPENDS + firebase_app + firebase_app_for_testing + firebase_testing +) + +firebase_cpp_cc_test(firebase_app_logger_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/logger_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_semaphore_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/semaphore_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_assert_test + SOURCES + assert_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_assert_release_test + SOURCES + assert_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_optional_test + SOURCES + optional_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_cleanup_notifier_test + SOURCES + cleanup_notifier_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_cpp11_thread_test + SOURCES + thread_test.cc + DEPENDS + firebase_app + gtest +) + +firebase_cpp_cc_test(firebase_app_pthread_thread_test + SOURCES + thread_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_time_tests + SOURCES + time_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_scheduler_test + SOURCES + scheduler_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_path_test + SOURCES + path_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_locale_test + SOURCES + locale_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_callback_test + SOURCES + callback_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_reference_count_test + SOURCES + reference_count_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_uuid_test + SOURCES + uuid_test.cc + DEPENDS + firebase_app + +) + +firebase_cpp_cc_test(firebase_app_variant_test + SOURCES + variant_test.cc + DEPENDS + firebase_app +) + +add_library(flexbuffer_matcher + flexbuffer_matcher.cc +) + +target_include_directories(flexbuffer_matcher + PRIVATE + ${FIREBASE_SOURCE_DIR} + ${FLATBUFFERS_SOURCE_DIR}/include +) + +target_link_libraries(flexbuffer_matcher + PRIVATE + flatbuffers + firebase_testing + gmock + gtest +) + +firebase_cpp_cc_test(flexbuffer_matcher_test + SOURCES + flexbuffer_matcher_test.cc + DEPENDS + firebase_app + firebase_testing + flexbuffer_matcher + flatbuffers +) + +firebase_cpp_cc_test(firebase_app_variant_util_tests + SOURCES + variant_util_test.cc + DEPENDS + firebase_app + firebase_testing + flexbuffer_matcher + flatbuffers +) + +if (NOT IOS AND APPLE) + add_library(firebase_app_secure_darwin_testlib + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/secure/user_secure_darwin_internal_testlib.mm + ) + target_include_directories(firebase_app_secure_darwin_testlib + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ) + target_link_libraries(firebase_app_secure_darwin_testlib + PUBLIC + "-framework Foundation" + "-framework Security" + ) + set(platform_secure_testlib firebase_app_secure_darwin_testlib) +else() + set(platform_secure_testlib) +endif() + +firebase_cpp_cc_test(firebase_app_user_secure_manager_test + SOURCES + secure/user_secure_manager_test.cc + DEPENDS + firebase_app + ${platform_secure_testlib} + DEFINES + -DUSER_SECURE_LOCAL_TEST +) + +if(FIREBASE_FORCE_FAKE_SECURE_STORAGE) + set(SECURE_STORAGE_DEFINES + -DFORCE_FAKE_SECURE_STORAGE + ) +endif() + +firebase_cpp_cc_test(firebase_app_user_secure_integration_test + SOURCES + secure/user_secure_integration_test.cc + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/secure/user_secure_fake_internal.cc + DEPENDS + firebase_app + firebase_testing + ${platform_secure_testlib} + INCLUDES + ${LIBSECRET_INCLUDE_DIRS} + DEFINES + -DUSER_SECURE_LOCAL_TEST + ${SECURE_STORAGE_DEFINES} +) + +firebase_cpp_cc_test(firebase_app_user_secure_internal_test + SOURCES + secure/user_secure_internal_test.cc + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/secure/user_secure_fake_internal.cc + DEPENDS + firebase_app + firebase_testing + ${platform_secure_testlib} + INCLUDES + ${LIBSECRET_INCLUDE_DIRS} + DEFINES + -DUSER_SECURE_LOCAL_TEST + ${SECURE_STORAGE_DEFINES} +) + +firebase_cpp_cc_test(firebase_app_memory_atomic_test + SOURCES + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/memory/atomic_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_memory_shared_ptr_test + SOURCES + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/memory/shared_ptr_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_memory_unique_ptr_test + SOURCES + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/memory/unique_ptr_test.cc + DEPENDS + firebase_app +) + +#[[ google3 Dependencies + +# google3 - FLAGS_test_srcdir +firebase_cpp_cc_test(firebase_app_google_services_test + SOURCES + google_services_test.cc + INCLUDES + ${FIREBASE_GEN_FILE_DIR} + DEPENDS + flatbuffers +) + +# google3 - FLAGS_test_srcdir +firebase_cpp_cc_test(firebase_app_desktop_test + SOURCES + app_test.cc + DEPENDS + firebase_app + firebase_testing +) + +# google3 - openssl/base64.h +firebase_cpp_cc_test(firebase_app_base64_test + SOURCES + base64_openssh_test.cc + base64_test.cc + INCLUDES + ${OPENSSL_INCLUDE_DIR} + DEPENDS + firebase_app + ${OPENSSL_CRYPTO_LIBRARY} +) + +# google3 - thread/fiber/fiber.h (thread::Fiber) +firebase_cpp_cc_test(firebase_app_future_playbillingclient_test + SOURCES + future_playbillingclient_test.cc + DEPENDS + firebase_app +) + +# google3 - thread/fiber/fiber.h (thread::Fiber) +firebase_cpp_cc_test(firebase_app_future_test + SOURCES + future_test.cc + DEPENDS + firebase_app +) + +# google3 - thread/fiber/fiber.h (thread::Fiber) +firebase_cpp_cc_test(firebase_app_future_manager_test + SOURCES + future_manager_test.cc + DEPENDS + firebase_app +) + + +# google3 Dependencies ]] + diff --git a/app/tests/app_test.cc b/app/tests/app_test.cc new file mode 100644 index 0000000000..910a9a253c --- /dev/null +++ b/app/tests/app_test.cc @@ -0,0 +1,599 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#ifndef __ANDROID__ +#define __ANDROID__ +#endif // __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/app_common.h" +#include "app/src/app_identifier.h" +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/version.h" +#include "app/src/include/firebase/internal/platform.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include +#include +#include +#include + +#include "flatbuffers/util.h" + +#if defined(_WIN32) +#include +#define getcwd _getcwd +#define chdir _chdir +#else +#include +#endif // defined(_WIN32) + +#include "testing/config.h" +#include "testing/ticker.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif // __APPLE__ + +#if FIREBASE_PLATFORM_IOS +// Declared in the Obj-C header fake/FIRApp.h. +extern "C" { +void FIRAppCreateUsingDefaultOptions(const char* name); +void FIRAppResetApps(); +} +#endif // FIREBASE_PLATFORM_IOS + +// FLAGS_test_srcdir is not defined on Android and iOS so we can't read +// from test resources. +#if ((defined(__APPLE__) && TARGET_OS_IOS) || defined(__ANDROID__) || \ + defined(FIREBASE_ANDROID_FOR_DESKTOP)) +#define TEST_RESOURCES_AVAILABLE 0 +#else +#define TEST_RESOURCES_AVAILABLE 1 +#endif // MOBILE + +using testing::ContainsRegex; +using testing::HasSubstr; +using testing::Not; + +namespace firebase { + +class AppTest : public ::testing::Test { + protected: + AppTest() : current_path_buffer_(nullptr) { +#if TEST_RESOURCES_AVAILABLE + test_data_dir_ = + FLAGS_test_srcdir + "/google3/firebase/app/client/cpp/testdata"; + broken_test_data_dir_ = test_data_dir_ + "/broken"; +#endif // TEST_RESOURCES_AVAILABLE + } + + void SetUp() override { + SaveCurrentDirectory(); +#if TEST_RESOURCES_AVAILABLE + EXPECT_EQ(chdir(test_data_dir_.c_str()), 0); +#endif // TEST_RESOURCES_AVAILABLE + } + + void TearDown() override { + RestoreCurrentDirectory(); + ClearAppInstances(); + } + + // Create a mobile app instance using the fake options from resources. + void CreateMobileApp(const char* name) { +#if FIREBASE_PLATFORM_IOS + FIRAppCreateUsingDefaultOptions(name ? name : "__FIRAPP_DEFAULT"); +#endif // FIREBASE_PLATFORM_IOS +#if FIREBASE_ANDROID_FOR_DESKTOP + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + jclass firebase_app_class = + env->FindClass("com/google/firebase/FirebaseApp"); + env->ExceptionCheck(); + jclass firebase_options_class = + env->FindClass("com/google/firebase/FirebaseOptions"); + env->ExceptionCheck(); + jobject options = env->CallStaticObjectMethod( + firebase_options_class, + env->GetStaticMethodID( + firebase_options_class, "fromResource", + "(Landroid/content/Context;)" + "Lcom/google/firebase/FirebaseOptions;"), + firebase::testing::cppsdk::GetTestActivity()); + env->ExceptionCheck(); + jobject app_name = env->NewStringUTF(name ? name : "[DEFAULT]"); + jobject app = env->CallStaticObjectMethod( + firebase_app_class, + env->GetStaticMethodID( + firebase_app_class, "initializeApp", + "(Landroid/content/Context;" + "Lcom/google/firebase/FirebaseOptions;" + "Ljava/lang/String;)Lcom/google/firebase/FirebaseApp;"), + firebase::testing::cppsdk::GetTestActivity(), + options, + app_name); + env->ExceptionCheck(); + env->DeleteLocalRef(app); + env->DeleteLocalRef(app_name); + env->DeleteLocalRef(options); + env->DeleteLocalRef(firebase_options_class); + env->DeleteLocalRef(firebase_app_class); +#endif // FIREBASE_ANDROID_FOR_DESKTOP + } + + private: + // Clear all C++ firebase::App objects and any mobile SDK instances. + void ClearAppInstances() { + app_common::DestroyAllApps(); +#if FIREBASE_PLATFORM_IOS + FIRAppResetApps(); +#endif // FIREBASE_PLATFORM_IOS +#if FIREBASE_ANDROID_FOR_DESKTOP + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + jclass firebase_app_class = + env->FindClass("com/google/firebase/FirebaseApp"); + env->ExceptionCheck(); + env->CallStaticVoidMethod( + firebase_app_class, + env->GetStaticMethodID(firebase_app_class, "reset", "()V")); + env->ExceptionCheck(); + env->DeleteLocalRef(firebase_app_class); +#endif // FIREBASE_ANDROID_FOR_DESKTOP + } + + void SaveCurrentDirectory() { + assert(current_path_buffer_ == nullptr); + current_path_buffer_ = new char[FILENAME_MAX]; + getcwd(current_path_buffer_, FILENAME_MAX); + } + + void RestoreCurrentDirectory() { + assert(current_path_buffer_ != nullptr); + EXPECT_EQ(chdir(current_path_buffer_), 0); + delete[] current_path_buffer_; + current_path_buffer_ = nullptr; + } + + protected: + char* current_path_buffer_; + std::string test_data_dir_; + std::string broken_test_data_dir_; +}; + +// The following few tests are testing the setter and getter of AppOptions. + +TEST_F(AppTest, TestSetAppId) { + AppOptions options; + options.set_app_id("abc"); + EXPECT_STREQ("abc", options.app_id()); +} + +TEST_F(AppTest, TestSetApiKey) { + AppOptions options; + options.set_api_key("AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk"); + EXPECT_STREQ("AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk", options.api_key()); +} + +TEST_F(AppTest, TestSetMessagingSenderId) { + AppOptions options; + options.set_messaging_sender_id("012345678901"); + EXPECT_STREQ("012345678901", options.messaging_sender_id()); +} + +TEST_F(AppTest, TestSetDatabaseUrl) { + AppOptions options; + options.set_database_url("http://abc-xyz-123.firebaseio.com"); + EXPECT_STREQ("http://abc-xyz-123.firebaseio.com", options.database_url()); +} + +TEST_F(AppTest, TestSetGaTrackingId) { + AppOptions options; + options.set_ga_tracking_id("UA-12345678-1"); + EXPECT_STREQ("UA-12345678-1", options.ga_tracking_id()); +} + +TEST_F(AppTest, TestSetStorageBucket) { + AppOptions options; + options.set_storage_bucket("abc-xyz-123.storage.firebase.com"); + EXPECT_STREQ("abc-xyz-123.storage.firebase.com", options.storage_bucket()); +} + +TEST_F(AppTest, TestSetProjectId) { + AppOptions options; + options.set_project_id("myproject-123"); + EXPECT_STREQ("myproject-123", options.project_id()); +} + +TEST_F(AppTest, LoadDefault) { + AppOptions options; + EXPECT_EQ(&options, + AppOptions::LoadDefault( + &options +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); + EXPECT_STREQ("fake app id from resource", options.app_id()); + EXPECT_STREQ("fake api key from resource", options.api_key()); + EXPECT_STREQ("fake messaging sender id from resource", + options.messaging_sender_id()); + EXPECT_STREQ("fake database url from resource", options.database_url()); +#if FIREBASE_PLATFORM_IOS + // GA tracking ID can currently only be configured on iOS. + EXPECT_STREQ("fake ga tracking id from resource", options.ga_tracking_id()); +#endif // FIREBASE_PLATFORM_IOS + EXPECT_STREQ("fake storage bucket from resource", options.storage_bucket()); + EXPECT_STREQ("fake project id from resource", options.project_id()); +#if !FIREBASE_PLATFORM_IOS + // The application bundle ID isn't available in iOS tests. + EXPECT_STRNE("", options.package_name()); +#endif // !FIREBASE_PLATFORM_IOS +} + +TEST_F(AppTest, PopulateRequiredWithDefaults) { + AppOptions options; + EXPECT_STREQ("", options.app_id()); + EXPECT_STREQ("", options.api_key()); + EXPECT_STREQ("", options.project_id()); + EXPECT_TRUE( + options.PopulateRequiredWithDefaults( +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); + EXPECT_STREQ("fake app id from resource", options.app_id()); + EXPECT_STREQ("fake api key from resource", options.api_key()); + EXPECT_STREQ("fake project id from resource", options.project_id()); +} + +// The following tests create Firebase App instances. + +// Helper functions to create test instance. +std::unique_ptr CreateFirebaseApp() { + return std::unique_ptr(App::Create( +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +std::unique_ptr CreateFirebaseApp(const char* name) { + return std::unique_ptr(App::Create( + AppOptions(), name +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +std::unique_ptr CreateFirebaseApp(const AppOptions& options) { + return std::unique_ptr(App::Create( + options +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +std::unique_ptr CreateFirebaseApp(const AppOptions& options, + const char* name) { + return std::unique_ptr(App::Create( + options, + name +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +TEST_F(AppTest, TestCreateDefault) { + // Created with default options. + std::unique_ptr firebase_app = CreateFirebaseApp(); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ(firebase::kDefaultAppName, firebase_app->name()); +} + +TEST_F(AppTest, TestCreateDefaultWithExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp(nullptr); + // Create the C++ proxy object, since we've specified no options this should + // return a proxy to the previously created object. + std::unique_ptr firebase_app = CreateFirebaseApp(); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ(firebase::kDefaultAppName, firebase_app->name()); + // Make sure the options loaded from the fake resource are present. + EXPECT_STREQ("fake project id from resource", + firebase_app->options().project_id()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateNamedWithExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp("a named app"); + // Create the C++ proxy object, since we've specified no options this should + // return a proxy to the previously created object. + std::unique_ptr firebase_app = CreateFirebaseApp("a named app"); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("a named app", firebase_app->name()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateWithOptions) { + // Created with options as well as name. + std::unique_ptr firebase_app = CreateFirebaseApp("my_apps_name"); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("my_apps_name", firebase_app->name()); +} + +TEST_F(AppTest, TestCreateDefaultWithDifferentOptionsToExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp(nullptr); + // Create the C++ proxy object, this should delete the previously created + // object returning a new object with the specified options. + AppOptions options; + options.set_api_key("an api key"); + options.set_app_id("a different app id"); + options.set_project_id("a project id"); + std::unique_ptr firebase_app = CreateFirebaseApp(options); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("__FIRAPP_DEFAULT", firebase_app->name()); + EXPECT_STREQ("an api key", firebase_app->options().api_key()); + EXPECT_STREQ("a different app id", firebase_app->options().app_id()); + EXPECT_STREQ("a project id", firebase_app->options().project_id()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateNamedWithDifferentOptionsToExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp("a named app"); + // Create the C++ proxy object, this should delete the previously created + // object returning a new object with the specified options. + AppOptions options; + options.set_api_key("an api key"); + options.set_app_id("a different app id"); + std::unique_ptr firebase_app = CreateFirebaseApp( + options, "a named app"); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("a named app", firebase_app->name()); + EXPECT_STREQ("a different app id", firebase_app->options().app_id()); + EXPECT_STREQ("an api key", firebase_app->options().api_key()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateMultipleTimes) { + // Created two apps with the same default name; the two are actually the same. + // We cannot use unique_ptr for this as the two will point to the same app. + App* firebase_app[2]; + for (int i = 0; i < 2; ++i) { + firebase_app[i] = App::Create( +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + ); + } + // There is only one app with the same name. + EXPECT_NE(nullptr, firebase_app[0]); + EXPECT_EQ(firebase_app[0], firebase_app[1]); + delete firebase_app[0]; +} + +// The following tests call GetInstance(). + +TEST_F(AppTest, TestGetDefaultInstance) { + // Nothing is created yet. We get nullptr. + EXPECT_EQ(nullptr, App::GetInstance()); + + // Now we create one. + std::unique_ptr firebase_app = CreateFirebaseApp(); + // We should get a non-nullptr pointer, which is what we created above. + EXPECT_NE(nullptr, App::GetInstance()); + EXPECT_EQ(firebase_app.get(), App::GetInstance()); + + // But there is one app for each distinct name. + EXPECT_EQ(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); +} + +TEST_F(AppTest, TestGetInstanceMultipleApps) { + // Nothing is created yet. + EXPECT_EQ(nullptr, App::GetInstance()); + EXPECT_EQ(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); + + // Now we create named app. + std::unique_ptr firebase_app = CreateFirebaseApp("thing_one"); + EXPECT_EQ(nullptr, App::GetInstance()); + EXPECT_NE(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(firebase_app.get(), App::GetInstance("thing_one")); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); + + // We again create a default app. + std::unique_ptr firebase_app_default = CreateFirebaseApp(); + EXPECT_NE(nullptr, App::GetInstance()); + EXPECT_EQ(firebase_app_default.get(), App::GetInstance()); + EXPECT_NE(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(firebase_app.get(), App::GetInstance("thing_one")); + EXPECT_NE(firebase_app, firebase_app_default); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); +} + +TEST_F(AppTest, TestParseUserAgent) { + app_common::RegisterLibrariesFromUserAgent("test/1 check/2 check/3"); + EXPECT_EQ(std::string(app_common::GetUserAgent()), + std::string("check/3 test/1")); +} + +TEST_F(AppTest, TestRegisterAndGetLibraryVersion) { + app_common::RegisterLibrary("a_library", "3.4.5"); + EXPECT_EQ("3.4.5", app_common::GetLibraryVersion("a_library")); + EXPECT_EQ("", app_common::GetLibraryVersion("a_non_existent_library")); +} + +TEST_F(AppTest, TestGetOuterMostSdkAndVersion) { + std::unique_ptr firebase_app_default = CreateFirebaseApp(); + std::string sdk; + std::string version; + app_common::GetOuterMostSdkAndVersion(&sdk, &version); + EXPECT_EQ(sdk, "fire-cpp"); + EXPECT_EQ(version, FIREBASE_VERSION_NUMBER_STRING); + app_common::RegisterLibrary("fire-mono", "4.5.6"); + app_common::GetOuterMostSdkAndVersion(&sdk, &version); + EXPECT_EQ(sdk, "fire-mono"); + EXPECT_EQ(version, "4.5.6"); + app_common::RegisterLibrary("fire-unity", "3.2.1"); + app_common::GetOuterMostSdkAndVersion(&sdk, &version); + EXPECT_EQ(sdk, "fire-unity"); + EXPECT_EQ(version, "3.2.1"); +} + +TEST_F(AppTest, TestRegisterLibrary) { + std::string firebase_version(std::string("fire-cpp/") + + std::string(FIREBASE_VERSION_NUMBER_STRING)); + std::unique_ptr firebase_app_default = CreateFirebaseApp(); + EXPECT_THAT(std::string(App::GetUserAgent()), HasSubstr(firebase_version)); + EXPECT_THAT(std::string(App::GetUserAgent()), + ContainsRegex("fire-cpp-os/(windows|darwin|linux|ios|android)")); + EXPECT_THAT(std::string(App::GetUserAgent()), + ContainsRegex("fire-cpp-arch/[^ ]+")); + EXPECT_THAT(std::string(App::GetUserAgent()), + ContainsRegex("fire-cpp-stl/[^ ]+")); + App::RegisterLibrary("fire-testing", "1.2.3"); + EXPECT_THAT(std::string(App::GetUserAgent()), + HasSubstr("fire-testing/1.2.3")); + firebase_app_default.reset(nullptr); + EXPECT_THAT(std::string(App::GetUserAgent()), + Not(HasSubstr("fire-testing/1.2.3"))); +} + +#if TEST_RESOURCES_AVAILABLE +TEST_F(AppTest, TestDefaultOptions) { + std::unique_ptr firebase_app = CreateFirebaseApp(AppOptions()); + + const AppOptions& options = firebase_app->options(); + EXPECT_STREQ("fake app id from resource", options.app_id()); + EXPECT_STREQ("fake api key from resource", options.api_key()); + EXPECT_STREQ("", options.messaging_sender_id()); + EXPECT_STREQ("", options.database_url()); + EXPECT_STREQ("", options.ga_tracking_id()); + EXPECT_STREQ("", options.storage_bucket()); + EXPECT_STREQ("fake project id from resource", options.project_id()); +} + +TEST_F(AppTest, TestReadOptionsFromResource) { + AppOptions app_options; + std::string json_file = test_data_dir_ + "/google-services.json"; + std::string config; + EXPECT_TRUE(flatbuffers::LoadFile(json_file.c_str(), false, &config)); + AppOptions::LoadFromJsonConfig(config.c_str(), &app_options); + std::unique_ptr firebase_app = CreateFirebaseApp(app_options); + + const AppOptions& options = firebase_app->options(); + // Check for the various fake options. + EXPECT_STREQ("fake mobilesdk app id", options.app_id()); + EXPECT_STREQ("fake api key", options.api_key()); + EXPECT_STREQ("fake project number", options.messaging_sender_id()); + EXPECT_STREQ("fake firebase url", options.database_url()); + // None of Firebase sample apps contain GA tracking_id. Looks like the field + // is either deprecated or not important. + EXPECT_STREQ("", options.ga_tracking_id()); + // Firebase auth sample app does not contain storage_bucket field. This could + // change and we should update here accordingly. + EXPECT_STREQ("", options.storage_bucket()); + EXPECT_STREQ("fake project id", options.project_id()); +} + +// Test that calling app.create() with no options tries to load from the local +// file google-services-desktop.json, before giving up. +TEST_F(AppTest, TestDefaultStart) { + // With no arguments, this will attempt to load a config from a file. + auto app = std::unique_ptr(App::Create()); + const AppOptions& options = app->options(); + EXPECT_STREQ(options.api_key(), "fake api key from resource"); + EXPECT_STREQ(options.storage_bucket(), "fake storage bucket from resource"); + EXPECT_STREQ(options.project_id(), "fake project id from resource"); + EXPECT_STREQ(options.database_url(), "fake database url from resource"); + EXPECT_STREQ(options.messaging_sender_id(), + "fake messaging sender id from resource"); +} + +TEST_F(AppTest, TestDefaultStartBrokenOptions) { + // Need to change the directory here to make sure we are in the same place + // as the broken google-services-desktop.json file. + EXPECT_EQ(chdir(broken_test_data_dir_.c_str()), 0); + // With no arguments, this will attempt to load a config from a file. + // This should fail as the file's format is invalid. + auto app = std::unique_ptr(App::Create()); + EXPECT_EQ(app.get(), nullptr); +} + +TEST_F(AppTest, TestCreateIdentifierFromOptions) { + { + AppOptions options; + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), ""); + } + { + AppOptions options; + options.set_package_name("org.foo.bar"); + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), + "org.foo.bar"); + } + { + AppOptions options; + options.set_project_id("cpp-sample-app-14e43"); + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), + "cpp-sample-app-14e43"); + } + { + AppOptions options; + options.set_project_id("cpp-sample-app-14e43"); + options.set_package_name("org.foo.bar"); + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), + "org.foo.bar.cpp-sample-app-14e43"); + } +} + +#endif // TEST_RESOURCES_AVAILABLE +} // namespace firebase diff --git a/app/tests/assert_test.cc b/app/tests/assert_test.cc new file mode 100644 index 0000000000..fc4c6b416a --- /dev/null +++ b/app/tests/assert_test.cc @@ -0,0 +1,283 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/assert.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::Ne; + +const char kTestMessage[] = "TEST_MESSAGE"; + +struct CallbackData { + LogLevel log_level; + std::string message; +}; + +void TestLogCallback(LogLevel log_level, const char* message, + void* callback_data) { + if (callback_data) { + auto* data = static_cast(callback_data); + data->log_level = log_level; + data->message = message; + } +} + +class AssertTest : public ::testing::Test { + public: + ~AssertTest() override { + LogSetCallback(nullptr, nullptr); + } +}; + +// Tests that check the functionality of FIREBASE_ASSERT_* macros in both debug +// and release builds. + +TEST_F(AssertTest, FirebaseAssertWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_WITH_EXPRESSION(false, FailureExpression), ""); +} + +TEST_F(AssertTest, FirebaseAssertAborts) { + EXPECT_DEATH(FIREBASE_ASSERT(false), ""); +} + +int FirebaseAssertReturnInt(int return_value) { + FIREBASE_ASSERT_RETURN(return_value, false); + return 0; +} + +TEST_F(AssertTest, FirebaseAssertReturnAborts) { + EXPECT_DEATH(FirebaseAssertReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseAssertReturnReturnsInt) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseAssertReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_RETURN_VOID(false), ""); +} + +void FirebaseAssertReturnVoid(int in_value, int* out_value) { + FIREBASE_ASSERT_RETURN_VOID(false); + *out_value = in_value; +} + +TEST_F(AssertTest, FirebaseAssertReturnVoidReturnsVoid) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseAssertMessageWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_MESSAGE_WITH_EXPRESSION( + false, FailureExpression, "Test Message: %s", kTestMessage), + ""); +} + +TEST_F(AssertTest, FirebaseAssertMessageAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_MESSAGE(false, "Test Message: %s", kTestMessage), + ""); +} + +int FirebaseAssertMessageReturnInt(int return_value) { + FIREBASE_ASSERT_MESSAGE_RETURN(return_value, false, "Test Message: %s", + kTestMessage); + return 0; +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnAborts) { + EXPECT_DEATH(FirebaseAssertMessageReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnReturnsInt) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertMessageReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_MESSAGE_RETURN_VOID(false, "Test Message: %s", + kTestMessage), + ""); +} + +void FirebaseAssertMessageReturnVoid(int in_value, int* out_value) { + FIREBASE_ASSERT_MESSAGE_RETURN_VOID(false, "Test Message: %s", kTestMessage); + *out_value = in_value; +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnVoidReturnsVoid) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertMessageReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +#if !defined(NDEBUG) + +// Tests that check the functionality of FIREBASE_DEV_ASSERT_* macros in debug +// builds only. + +TEST_F(AssertTest, FirebaseDevAssertWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_WITH_EXPRESSION(false, FailureExpression), + ""); +} + +TEST_F(AssertTest, FirebaseDevAssertAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT(false), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnAborts) { + EXPECT_DEATH(FirebaseAssertReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnReturnsInt) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseDevAssertReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_RETURN_VOID(false), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnVoidReturnsVoid) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseDevAssertMessageWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_MESSAGE_WITH_EXPRESSION( + false, FailureExpression, "Test Message: %s", kTestMessage), + ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageAborts) { + EXPECT_DEATH( + FIREBASE_DEV_ASSERT_MESSAGE(false, "Test Message: %s", kTestMessage), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnAborts) { + EXPECT_DEATH(FirebaseAssertMessageReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnReturnsInt) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertMessageReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_MESSAGE_RETURN_VOID( + false, "Test Message: %s", kTestMessage), + ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnVoidReturnsVoid) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertMessageReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +#else + +// Tests that check that FIREBASE_DEV_ASSERT_* macros are compiled out of +// release builds. + +TEST_F(AssertTest, FirebaseDevAssertWithExpressionCompiledOut) { + FIREBASE_DEV_ASSERT_WITH_EXPRESSION(false, FailureExpression); +} + +TEST_F(AssertTest, FirebaseDevAssertCompiledOut) { FIREBASE_DEV_ASSERT(false); } + +TEST_F(AssertTest, FirebaseDevAssertReturnCompiledOut) { + FIREBASE_DEV_ASSERT_RETURN(1, false); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnVoidCompiledOut) { + FIREBASE_DEV_ASSERT_RETURN_VOID(false); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageWithExpressionCompiledOut) { + FIREBASE_DEV_ASSERT_MESSAGE_WITH_EXPRESSION(false, FailureExpression, + "Test Message: %s", kTestMessage); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageCompiledOut) { + FIREBASE_DEV_ASSERT_MESSAGE(false, "Test Message: %s", kTestMessage); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnCompiledOut){ + FIREBASE_DEV_ASSERT_MESSAGE_RETURN(1, false, "Test Message: %s", + kTestMessage)} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnVoidAborts) { + FIREBASE_DEV_ASSERT_MESSAGE_RETURN_VOID(false, "Test Message: %s", + kTestMessage); +} + +#endif // !defined(NDEBUG) + +} // namespace +} // namespace firebase diff --git a/app/tests/base64_openssh_test.cc b/app/tests/base64_openssh_test.cc new file mode 100644 index 0000000000..b415c6f92d --- /dev/null +++ b/app/tests/base64_openssh_test.cc @@ -0,0 +1,98 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/base64.h" +#include "app/src/log.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "openssl/base64.h" + +namespace firebase { +namespace internal { + +size_t OpenSSHEncodedLength(size_t input_size) { + size_t length; + if (!EVP_EncodedLength(&length, input_size)) { + return 0; + } + return length; +} + +bool OpenSSHEncode(const std::string& input, std::string* output) { + size_t base64_length = OpenSSHEncodedLength(input.size()); + output->resize(base64_length); + if (EVP_EncodeBlock(reinterpret_cast(&(*output)[0]), + reinterpret_cast(&input[0]), + input.size()) == 0u) { + return false; + } + // Trim the terminating null character. + output->resize(base64_length - 1); + return true; +} + +size_t OpenSSHDecodedLength(size_t input_size) { + size_t length; + if (!EVP_DecodedLength(&length, input_size)) { + return 0; + } + return length; +} + +bool OpenSSHDecode(const std::string& input, std::string* output) { + size_t decoded_length = OpenSSHDecodedLength(input.size()); + output->resize(decoded_length); + if (EVP_DecodeBase64(reinterpret_cast(&(*output)[0]), + &decoded_length, decoded_length, + reinterpret_cast(&(input)[0]), + input.size()) == 0) { + return false; + } + // Decoded length includes null termination, remove. + output->resize(decoded_length); + return true; +} + +TEST(Base64TestAgainstOpenSSH, TestEncodingAgainstOpenSSH) { + // Run this test 100 times. + for (int i = 0; i < 100; i++) { + // Generate 1-10000 random bytes. OpenSSH can't encode an empty string. + size_t bytes = 1 + rand() % 9999; // NOLINT + std::string orig; + orig.resize(bytes); + for (int c = 0; c < orig.size(); ++c) { + orig[c] = rand() % 0xFF; // NOLINT + } + + std::string encoded_firebase, encoded_openssh; + ASSERT_TRUE(Base64EncodeWithPadding(orig, &encoded_firebase)); + ASSERT_TRUE(OpenSSHEncode(orig, &encoded_openssh)); + EXPECT_EQ(encoded_firebase, encoded_openssh) + << "Encoding mismatch on source buffer: " << orig; + + std::string decoded_firebase_to_openssh; + std::string decoded_openssh_to_firebase; + ASSERT_TRUE(Base64Decode(encoded_openssh, &decoded_openssh_to_firebase)); + ASSERT_TRUE(OpenSSHDecode(encoded_firebase, &decoded_firebase_to_openssh)); + EXPECT_EQ(decoded_openssh_to_firebase, decoded_firebase_to_openssh) + << "Cross-decoding mismatch on source buffer: " << orig; + EXPECT_EQ(orig, decoded_firebase_to_openssh); + EXPECT_EQ(orig, decoded_openssh_to_firebase); + } +} + +} // namespace internal +} // namespace firebase diff --git a/app/tests/base64_test.cc b/app/tests/base64_test.cc new file mode 100644 index 0000000000..3e309a6e47 --- /dev/null +++ b/app/tests/base64_test.cc @@ -0,0 +1,221 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/base64.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace internal { + +TEST(Base64Test, EncodeAndDecodeText) { + // Test 3 different lengths of string to ensure that trailing = is handled + // correctly. + const std::string kOrig0("Hello, world!"), kEncoded0("SGVsbG8sIHdvcmxkIQ"); + const std::string kOrig1("How are you?"), kEncoded1("SG93IGFyZSB5b3U/"); + const std::string kOrig2("I'm fine..."), kEncoded2("SSdtIGZpbmUuLi4"); + + std::string encoded, decoded; + EXPECT_TRUE(Base64Encode(kOrig0, &encoded)); + EXPECT_EQ(encoded, kEncoded0); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig0); + + EXPECT_TRUE(Base64Encode(kOrig1, &encoded)); + EXPECT_EQ(encoded, kEncoded1); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig1); + + EXPECT_TRUE(Base64Encode(kOrig2, &encoded)); + EXPECT_EQ(encoded, kEncoded2); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig2); +} + +TEST(Base64Test, EncodeAndDecodeTextWithPadding) { + // Test 3 different lengths of string to ensure that trailing = is handled + // correctly. + const std::string kOrig0("Hello, world!"), kEncoded0("SGVsbG8sIHdvcmxkIQ=="); + const std::string kOrig1("How are you?"), kEncoded1("SG93IGFyZSB5b3U/"); + const std::string kOrig2("I'm fine..."), kEncoded2("SSdtIGZpbmUuLi4="); + + std::string encoded, decoded; + EXPECT_TRUE(Base64EncodeWithPadding(kOrig0, &encoded)); + EXPECT_EQ(encoded, kEncoded0); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig0); + + EXPECT_TRUE(Base64EncodeWithPadding(kOrig1, &encoded)); + EXPECT_EQ(encoded, kEncoded1); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig1); + + EXPECT_TRUE(Base64EncodeWithPadding(kOrig2, &encoded)); + EXPECT_EQ(encoded, kEncoded2); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig2); +} + +TEST(Base64Test, SmallEncodeAndDecode) { + const std::string kEmpty; + std::string encoded, decoded; + EXPECT_TRUE(Base64Encode(kEmpty, &encoded)); + EXPECT_EQ(encoded, kEmpty); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode empty"; + EXPECT_EQ(decoded, kEmpty); + + EXPECT_TRUE(Base64EncodeWithPadding("\xFF", &encoded)); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, "\xFF"); + EXPECT_TRUE(Base64EncodeWithPadding("\xFF\xA0", &encoded)); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, "\xFF\xA0"); +} + +TEST(Base64Test, FullCharacterSet) { + // Ensure all 64 possible characters are properly parsed in all 4 positions. + const std::string kEncoded( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABC"); + std::string decoded, encoded; + EXPECT_TRUE(Base64Decode(kEncoded, &decoded)) + << "Couldn't decode " << kEncoded; + EXPECT_TRUE(Base64EncodeWithPadding(decoded, &encoded)); + EXPECT_EQ(encoded, kEncoded); +} + +TEST(Base64Test, BinaryEncodeAndDecode) { + // Check binary string. + const char kBinaryData[] = + "\x00\x05\x20\x3C\x40\x45\x50\x60\x70\x80\x90\x00\xA0\xB5\xC2\xD1\xF0" + "\xFF\x00\xE0\x42"; + + const std::string kBinaryOrig(kBinaryData, sizeof(kBinaryData) - 1); + const std::string kBinaryEncoded = "AAUgPEBFUGBwgJAAoLXC0fD/AOBC"; + std::string encoded, decoded; + + EXPECT_TRUE(Base64Encode(kBinaryOrig, &encoded)); + EXPECT_EQ(encoded, kBinaryEncoded); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kBinaryOrig); +} + +TEST(Base64Test, InPlaceEncodeAndDecode) { + const std::string kOrig("Hello, world!"), kEncoded("SGVsbG8sIHdvcmxkIQ"), + kEncodedWithPadding("SGVsbG8sIHdvcmxkIQ=="); + + // Ensure we can encode and decode in-place in the same buffer. + std::string buffer = kOrig; + EXPECT_TRUE(Base64Encode(buffer, &buffer)); + EXPECT_EQ(buffer, kEncoded); + EXPECT_TRUE(Base64Decode(buffer, &buffer)); + EXPECT_EQ(buffer, kOrig); + EXPECT_TRUE(Base64EncodeWithPadding(buffer, &buffer)); + EXPECT_EQ(buffer, kEncodedWithPadding); + EXPECT_TRUE(Base64Decode(buffer, &buffer)); + EXPECT_EQ(buffer, kOrig); +} + +TEST(Base64Test, FailToEncode) { + EXPECT_FALSE(Base64Encode("Hello", nullptr)); + EXPECT_FALSE(Base64EncodeWithPadding("Hello", nullptr)); +} + +TEST(Base64Test, FailToDecode) { + // Test some malformed base64. + std::string unused; + EXPECT_FALSE(Base64Decode("BadCharacterCountHere", &unused)); + EXPECT_FALSE(Base64Decode("HasEqual=SignInTheMiddle", &unused)); + EXPECT_FALSE(Base64Decode("EqualsFourFromEndA==AAAA", &unused)); + EXPECT_FALSE(Base64Decode("EqualsFourFromEndAA=AAAA", &unused)); + EXPECT_FALSE(Base64Decode("HasTooManyEqualsSignA===", &unused)); + EXPECT_FALSE(Base64Decode("PenultimateEqualsOnlyO=o", &unused)); + EXPECT_FALSE(Base64Decode("HasAnIncompatible$Symbol", &unused)); + + // Decoding should fail if there are any dangling '1' bits past the end of the + // encoded text. + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a==", &unused)); + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a", &unused)); + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a/=", &unused)); + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a/", &unused)); + + // Too short. + EXPECT_FALSE(Base64Decode("a", &unused)); + + // Test passing in nullptr as output. + EXPECT_FALSE(Base64Decode("abcd", nullptr)); +} + +TEST(Base64Test, TestSizeCalculations) { + EXPECT_EQ(GetBase64EncodedSize(""), 0); + EXPECT_EQ(GetBase64EncodedSize("a"), 4); + EXPECT_EQ(GetBase64EncodedSize("aa"), 4); + EXPECT_EQ(GetBase64EncodedSize("aaa"), 4); + EXPECT_EQ(GetBase64EncodedSize("aaaa"), 8); + EXPECT_EQ(GetBase64EncodedSize("aaaaa"), 8); + EXPECT_EQ(GetBase64EncodedSize("aaaaaa"), 8); + EXPECT_EQ(GetBase64EncodedSize("aaaaaaa"), 12); + + EXPECT_EQ(GetBase64DecodedSize(""), 0); + EXPECT_EQ(GetBase64DecodedSize("A"), 0); + EXPECT_EQ(GetBase64DecodedSize("AA"), 1); + EXPECT_EQ(GetBase64DecodedSize("AA=="), 1); + EXPECT_EQ(GetBase64DecodedSize("AAA"), 2); + EXPECT_EQ(GetBase64DecodedSize("AAA="), 2); + EXPECT_EQ(GetBase64DecodedSize("AAAA"), 3); + EXPECT_EQ(GetBase64DecodedSize("AAAAA"), 0); + EXPECT_EQ(GetBase64DecodedSize("AAAAAA"), 4); + EXPECT_EQ(GetBase64DecodedSize("AAAAAA=="), 4); + EXPECT_EQ(GetBase64DecodedSize("AAAAAAA"), 5); + EXPECT_EQ(GetBase64DecodedSize("AAAAAAA="), 5); + EXPECT_EQ(GetBase64DecodedSize("AAAAAAAA"), 6); +} + +TEST(Base64Test, TestUrlSafeEncoding) { + const std::string kEncoded( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABCAA"); + const std::string kEncodedUrlSafe( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ABCAA"); + const std::string kEncodedUrlSafeWithPadding( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ABCAA=="); + std::string decoded, decoded_urlsafe; + EXPECT_TRUE(Base64Decode(kEncoded, &decoded)); + EXPECT_TRUE(Base64Decode(kEncodedUrlSafe, &decoded_urlsafe)); + EXPECT_EQ(decoded_urlsafe, decoded); + + std::string encoded_urlsafe; + EXPECT_TRUE(Base64EncodeUrlSafe(decoded, &encoded_urlsafe)); + EXPECT_EQ(encoded_urlsafe, kEncodedUrlSafe); + + std::string encoded_urlsafe_padded; + EXPECT_TRUE(Base64EncodeUrlSafeWithPadding(decoded, &encoded_urlsafe_padded)); + EXPECT_EQ(encoded_urlsafe_padded, kEncodedUrlSafeWithPadding); +} + +} // namespace internal +} // namespace firebase diff --git a/app/tests/callback_test.cc b/app/tests/callback_test.cc new file mode 100644 index 0000000000..f1175f966e --- /dev/null +++ b/app/tests/callback_test.cc @@ -0,0 +1,462 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/callback.h" + +#include +#include + +#include "app/memory/unique_ptr.h" +#include "app/src/mutex.h" +#include "app/src/thread.h" +#include "app/src/time.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; + +namespace firebase { + +class CallbackTest : public ::testing::Test { + protected: + CallbackTest() {} + + void SetUp() override { + callback_void_count_ = 0; + callback1_count_ = 0; + callback_value1_sum_ = 0; + callback_value1_ordered_.clear(); + callback_value2_sum_ = 0; + callback_string_.clear(); + value_and_string_ = std::pair(); + } + + // Counts callbacks from callback::CallbackVoid. + static void CountCallbackVoid() { callback_void_count_++; } + // Counts callbacks from callback::Callback1. + static void CountCallback1(void* test) { + CallbackTest* callback_test = *(static_cast(test)); + callback_test->callback1_count_++; + } + // Adds the value passed to CallbackValue1 to callback_value1_sum_. + static void SumCallbackValue1(int value) { callback_value1_sum_ += value; } + + // Add the value passed to CallbackValue1 to callback_value1_ordered_. + static void OrderedCallbackValue1(int value) { + callback_value1_ordered_.push_back(value); + } + + // Multiplies values passed to CallbackValue2 and adds them to + // callback_value2_sum_. + static void SumCallbackValue2(char value1, int value2) { + callback_value2_sum_ += value1 * value2; + } + + // Appends the string passed to this method to callback_string_. + static void AggregateCallbackString(const char* str) { + callback_string_ += str; + } + + // Stores this function's arguments in value_and_string_. + static void StoreValueAndString(int value, const char* str) { + value_and_string_ = std::pair(value, str); + } + + // Stores the value argument in value_and_string_.first, then appends the two + // string arguments and assign to value_and_string_.second, + static void StoreValueAndString2(const char* str1, const char* str2, + int value) { + value_and_string_ = std::pair( + value, std::string(str1) + std::string(str2)); + } + + // Stores the sum of value1 and value2 in value_and_string_.first and the + // string argumene in value_and_string_.second. + static void StoreValue2AndString(char value1, int value2, const char* str) { + value_and_string_ = std::pair(value1 + value2, str); + } + + // Adds the value passed to CallbackMoveValue1 to callback_value1_sum_. + static void SumCallbackMoveValue1(UniquePtr* value) { + callback_value1_sum_ += **value; + } + + int callback1_count_; + + static int callback_void_count_; + static int callback_value1_sum_; + static std::vector callback_value1_ordered_; + static int callback_value2_sum_; + static std::string callback_string_; + static std::pair value_and_string_; +}; + +int CallbackTest::callback_value1_sum_; +std::vector CallbackTest::callback_value1_ordered_; // NOLINT +int CallbackTest::callback_value2_sum_; +int CallbackTest::callback_void_count_; +std::string CallbackTest::callback_string_; // NOLINT +std::pair CallbackTest::value_and_string_; // NOLINT + +// Verify initialize and terminate setup and tear down correctly. +TEST_F(CallbackTest, TestInitializeAndTerminate) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::Initialize(); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::Terminate(false); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Verify Terminate() is a no-op if the API isn't initialized. +TEST_F(CallbackTest, TestTerminateWithoutInitialization) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::Terminate(false); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Add a callback to the queue then terminate the API. +TEST_F(CallbackTest, AddCallbackNoInitialization) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::Terminate(false); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Flush all callbacks. +TEST_F(CallbackTest, AddCallbacksTerminateAndFlush) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::PollCallbacks(); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::Terminate(true); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Add a callback to the queue, then remove it. This should result in +// initializing the callback API then tearing it down when the queue is empty. +TEST_F(CallbackTest, AddRemoveCallback) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + void* callback_reference = + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::RemoveCallback(callback_reference); + callback::PollCallbacks(); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + EXPECT_THAT(callback_void_count_, Eq(0)); +} + +// Call a void callback. +TEST_F(CallbackTest, CallVoidCallback) { + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call two void callbacks. +TEST_F(CallbackTest, CallTwoVoidCallbacks) { + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call three void callbacks with a poll between them. +TEST_F(CallbackTest, CallOneVoidCallbackPollTwo) { + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(3)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call 2, 1 argument callbacks. +TEST_F(CallbackTest, CallCallback1) { + callback::AddCallback( + new callback::Callback1(this, CountCallback1)); + callback::AddCallback( + new callback::Callback1(this, CountCallback1)); + callback::PollCallbacks(); + EXPECT_THAT(callback1_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing the argument by value. +TEST_F(CallbackTest, CallCallbackValue1) { + callback::AddCallback( + new callback::CallbackValue1(10, SumCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(5, SumCallbackValue1)); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(15)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Ensure callbacks are executed in the order they're added to the queue. +TEST_F(CallbackTest, CallCallbackValue1Ordered) { + callback::AddCallback( + new callback::CallbackValue1(10, OrderedCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(5, OrderedCallbackValue1)); + callback::PollCallbacks(); + std::vector expected; + expected.push_back(10); + expected.push_back(5); + EXPECT_THAT(callback_value1_ordered_, Eq(expected)); +} + +// Schedule 3 callbacks, removing the middle one from the queue. +TEST_F(CallbackTest, ScheduleThreeCallbacksRemoveOne) { + callback::AddCallback( + new callback::CallbackValue1(1, SumCallbackValue1)); + void* reference = callback::AddCallback( + new callback::CallbackValue1(2, SumCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(4, SumCallbackValue1)); + callback::RemoveCallback(reference); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(5)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing two arguments by value. +TEST_F(CallbackTest, CallCallbackValue2) { + callback::AddCallback( + new callback::CallbackValue2(10, 4, SumCallbackValue2)); + callback::AddCallback( + new callback::CallbackValue2(20, 3, SumCallbackValue2)); + callback::PollCallbacks(); + EXPECT_THAT(callback_value2_sum_, Eq(100)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing a string by value. +TEST_F(CallbackTest, CallCallbackString) { + callback::AddCallback( + new callback::CallbackString("testing", AggregateCallbackString)); + callback::AddCallback( + new callback::CallbackString("123", AggregateCallbackString)); + callback::PollCallbacks(); + EXPECT_THAT(callback_string_, Eq("testing123")); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing a value and string by value. +TEST_F(CallbackTest, CallCallbackValue1String1) { + callback::AddCallback( + new callback::CallbackValue1String1(10, "ten", StoreValueAndString)); + callback::PollCallbacks(); + EXPECT_THAT(value_and_string_.first, Eq(10)); + EXPECT_THAT(value_and_string_.second, Eq("ten")); +} + +// Call a callback passing a value and two strings by value. +TEST_F(CallbackTest, CallCallbackString2Value1) { + callback::AddCallback(new callback::CallbackString2Value1( + "evening", "all", 11, StoreValueAndString2)); + callback::PollCallbacks(); + EXPECT_THAT(value_and_string_.first, Eq(11)); + EXPECT_THAT(value_and_string_.second, Eq("eveningall")); +} + +// Call a callback passing two values and a string by value. +TEST_F(CallbackTest, CallCallbackValue2String1) { + callback::AddCallback(new callback::CallbackValue2String1( + 11, 31, "meaning", StoreValue2AndString)); + callback::PollCallbacks(); + EXPECT_THAT(value_and_string_.first, Eq(42)); + EXPECT_THAT(value_and_string_.second, Eq("meaning")); +} + +// Call a callback passing the UniquePtr +TEST_F(CallbackTest, CallCallbackMoveValue1) { + callback::AddCallback(new callback::CallbackMoveValue1>( + MakeUnique(10), SumCallbackMoveValue1)); + UniquePtr ptr(new int(5)); + callback::AddCallback(new callback::CallbackMoveValue1>( + Move(ptr), SumCallbackMoveValue1)); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(15)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +#ifdef FIREBASE_USE_STD_FUNCTION +// Call a callback which wraps std::function +TEST_F(CallbackTest, CallCallbackStdFunction) { + int count = 0; + std::function callback = [&count]() { count++; }; + + callback::AddCallback(new callback::CallbackStdFunction(callback)); + callback::PollCallbacks(); + EXPECT_THAT(count, Eq(1)); + callback::AddCallback(new callback::CallbackStdFunction(callback)); + callback::AddCallback(new callback::CallbackStdFunction(callback)); + callback::PollCallbacks(); + EXPECT_THAT(count, Eq(3)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} +#endif + +// Ensure callbacks are executed in the order they're added to the queue with +// callbacks added to a different thread to the dispatching thread. +// Also, make sure it's possible to remove a callback from the queue while +// executing a callback. +TEST_F(CallbackTest, ThreadedCallbackValue1Ordered) { + bool running = true; + void* callback_entry_to_remove = nullptr; + Thread pollingThread( + [](void* arg) -> void { + volatile bool* running_ptr = static_cast(arg); + while (*running_ptr) { + callback::PollCallbacks(); + // Wait 20ms. + ::firebase::internal::Sleep(20); + } + }, + &running); + Thread addCallbacksThread( + [](void* arg) -> void { + void** callback_entry_to_remove_ptr = static_cast(arg); + callback::AddCallback( + new callback::CallbackValue1(1, OrderedCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(2, OrderedCallbackValue1)); + // Adds a callback which removes the entry referenced by + // callback_entry_to_remove. + callback::AddCallback(new callback::CallbackValue1( + callback_entry_to_remove_ptr, [](void** callback_entry) -> void { + callback::RemoveCallback(*callback_entry); + })); + *callback_entry_to_remove_ptr = callback::AddCallback( + new callback::CallbackValue1(4, OrderedCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(5, OrderedCallbackValue1)); + }, + &callback_entry_to_remove); + addCallbacksThread.Join(); + callback::AddCallback(new callback::CallbackValue1( + &running, [](volatile bool* running_ptr) { *running_ptr = false; })); + pollingThread.Join(); + std::vector expected; + expected.push_back(1); + expected.push_back(2); + expected.push_back(5); + EXPECT_THAT(callback_value1_ordered_, Eq(expected)); +} + +TEST_F(CallbackTest, NewCallbackTest) { + callback::AddCallback(callback::NewCallback(SumCallbackValue1, 1)); + callback::AddCallback(callback::NewCallback(SumCallbackValue1, 2)); + callback::AddCallback( + callback::NewCallback(SumCallbackValue2, static_cast(1), 10)); + callback::AddCallback( + callback::NewCallback(SumCallbackValue2, static_cast(2), 100)); + callback::AddCallback( + callback::NewCallback(AggregateCallbackString, "Hello, ")); + callback::AddCallback( + callback::NewCallback(AggregateCallbackString, "World!")); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(3)); + EXPECT_THAT(callback_value2_sum_, Eq(210)); + EXPECT_THAT(callback_string_, testing::StrEq("Hello, World!")); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +TEST_F(CallbackTest, AddCallbackWithThreadCheckTest) { + // When PollCallbacks() is called in previous test, g_callback_thread_id + // would be set to current thread which runs the tests. We want it to be set + // to a different thread id in the beginning of this test. + Thread changeThreadIdThread([]() { + callback::AddCallback(new callback::CallbackVoid([](){})); + callback::PollCallbacks(); + }); + changeThreadIdThread.Join(); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + + void* entry_non_null = callback::AddCallbackWithThreadCheck( + new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_TRUE(entry_non_null != nullptr); + EXPECT_THAT(callback_void_count_, Eq(0)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + + // Once PollCallbacks() is called on this thread, AddCallbackWithThreadCheck() + // should run the callback immediately and return nullptr. + void* entry_null = callback::AddCallbackWithThreadCheck( + new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_TRUE(entry_null == nullptr); + EXPECT_THAT(callback_void_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +TEST_F(CallbackTest, CallbackDeadlockTest) { + // This is to test the deadlock scenario when CallbackEntry::Execute() and + // CallbackEntry::DisableCallback() are called at the same time. + // Ex. given a user mutex "user_mutex" + // GC thread: lock(user_mutex) -> lock(CallbackEntry::mutex_) + // Polling thread: lock(CallbackEntry::mutex_) -> lock(user_mutex) + // If both threads successfully obtain the first lock, a deadlock could occur. + // CallbackEntry::mutex_ should be released while running the callback. + + struct DeadlockData { + Mutex user_mutex; + void* handle; + }; + + for (int i = 0; i < 1000; ++i) { + DeadlockData data; + + data.handle = + callback::AddCallback(new callback::CallbackValue1( + &data, [](DeadlockData* data) { + MutexLock lock(data->user_mutex); + data->handle = nullptr; + })); + + Thread pollingThread([]() { callback::PollCallbacks(); }); + + Thread gcThread( + [](void* arg) { + DeadlockData* data = static_cast(arg); + MutexLock lock(data->user_mutex); + if (data->handle) { + callback::RemoveCallback(data->handle); + } + }, + &data); + + pollingThread.Join(); + gcThread.Join(); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + } +} +} // namespace firebase diff --git a/app/tests/cleanup_notifier_test.cc b/app/tests/cleanup_notifier_test.cc new file mode 100644 index 0000000000..52f7179cb9 --- /dev/null +++ b/app/tests/cleanup_notifier_test.cc @@ -0,0 +1,417 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/cleanup_notifier.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace testing { + +class CleanupNotifierTest : public ::testing::Test {}; + +namespace { +struct Object { + explicit Object(int counter_) : counter(counter_) {} + int counter; + + static void IncrementCounter(void* obj_void) { + reinterpret_cast(obj_void)->counter++; + } + static void DecrementCounter(void* obj_void) { + reinterpret_cast(obj_void)->counter--; + } +}; +} // namespace + +TEST_F(CleanupNotifierTest, TestCallbacksAreCalledAutomatically) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj, Object::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestCallbacksAreCalledManuallyOnceOnly) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj, Object::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + } + // Ensure the callback isn't called again. + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestCallbacksCanBeUnregistered) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj, Object::IncrementCounter); + cleanup.UnregisterObject(&obj); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(CleanupNotifierTest, TestMultipleObjects) { + Object obj1(1), obj2(2); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, Object::IncrementCounter); + cleanup.RegisterObject(&obj2, Object::IncrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(CleanupNotifierTest, TestMultipleCallbacksMultipleObjects) { + Object obj1(1), obj2(2); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, Object::IncrementCounter); + cleanup.RegisterObject(&obj2, Object::DecrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestOnlyOneCallbackPerObject) { + Object obj1(1), obj2(2); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, Object::IncrementCounter); + cleanup.RegisterObject(&obj2, Object::IncrementCounter); + // The following call overwrites the previous callback on obj1: + cleanup.RegisterObject(&obj1, Object::DecrementCounter); + EXPECT_EQ(obj1.counter, 1); // Has not run. + } + EXPECT_EQ(obj1.counter, 0); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(CleanupNotifierTest, TestDoesNotCrashWhenYouUnregisterInvalidObject) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + } + EXPECT_EQ(obj.counter, 0); + { + CleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + cleanup.RegisterObject(&obj, Object::IncrementCounter); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestDoesNotCrashIfCallingZeroCallbacks) { + Object obj(0); + { CleanupNotifier cleanup; } + { + CleanupNotifier cleanup; + cleanup.CleanupAll(); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(CleanupNotifierTest, TestMultipleCleanupNotifiersReferringToSameObject) { + Object obj(0); + { + CleanupNotifier cleanup1, cleanup2; + cleanup1.RegisterObject(&obj, Object::IncrementCounter); + cleanup2.RegisterObject(&obj, Object::IncrementCounter); + } + EXPECT_EQ(obj.counter, 2); +} + +namespace { +class OwnerObject { + public: + OwnerObject() { notifier_.RegisterOwner(this); } + ~OwnerObject() { notifier_.CleanupAll(); } + + protected: + CleanupNotifier notifier_; +}; + +class DerivedOwnerObject : public OwnerObject { + public: + DerivedOwnerObject() { notifier_.RegisterOwner(this); } + ~DerivedOwnerObject() {} +}; + +class SubscriberObject { + public: + SubscriberObject(void* subscribe_for_cleanup_object, + bool* flag_to_set_on_cleanup) + : subscribe_for_cleanup_object_(subscribe_for_cleanup_object), + flag_to_set_on_cleanup_(flag_to_set_on_cleanup) { + CleanupNotifier* notifier = + CleanupNotifier::FindByOwner(subscribe_for_cleanup_object_); + EXPECT_TRUE(notifier != nullptr); + notifier->RegisterObject(this, [](void* object) { + delete reinterpret_cast(object); + }); + } + + ~SubscriberObject() { + CleanupNotifier* notifier = + CleanupNotifier::FindByOwner(subscribe_for_cleanup_object_); + EXPECT_TRUE(notifier != nullptr); + notifier->UnregisterObject(this); + *flag_to_set_on_cleanup_ = true; + } + + private: + void* subscribe_for_cleanup_object_; + bool* flag_to_set_on_cleanup_; +}; +} // namespace + +class CleanupNotifierOwnerRegistryTest : public ::testing::Test {}; + +// Validate registration and retrieval of owner objects. +TEST_F(CleanupNotifierOwnerRegistryTest, RegisterAndFindByOwner) { + int owner1 = 1; + int owner2 = 2; + int owner3 = 3; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner3)); + { + CleanupNotifier notifier1; + { + CleanupNotifier notifier2; + notifier1.RegisterOwner(&owner1); + notifier1.RegisterOwner(&owner2); + notifier2.RegisterOwner(&owner2); + notifier2.RegisterOwner(&owner3); + EXPECT_EQ(¬ifier1, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(¬ifier2, CleanupNotifier::FindByOwner(&owner3)); + // Registration with notifier2 overrides owner2 association with + // notifier1. + EXPECT_EQ(¬ifier2, CleanupNotifier::FindByOwner(&owner2)); + } + EXPECT_EQ(¬ifier1, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner3)); + } + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, RegisterAndUnregisterByOwner) { + int owner1 = 1; + int owner2 = 2; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + { + CleanupNotifier notifier; + notifier.RegisterOwner(&owner1); + notifier.RegisterOwner(&owner2); + EXPECT_EQ(¬ifier, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(¬ifier, CleanupNotifier::FindByOwner(&owner2)); + notifier.UnregisterOwner(&owner2); + EXPECT_EQ(¬ifier, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + } + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, CleanupRegistrationByOwnerObject) { + void* owner_pointer = nullptr; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); + Object cleanup_object(0); + { + OwnerObject owner; + owner_pointer = &owner; + // The cleanup notifier is not part of the public API of OwnerObject so we + // find it via a pointer to the object in the global registry. + CleanupNotifier* notifier = CleanupNotifier::FindByOwner(owner_pointer); + EXPECT_TRUE(notifier != nullptr); + notifier->RegisterObject(&cleanup_object, Object::IncrementCounter); + } + EXPECT_EQ(1, cleanup_object.counter); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, CleanupRegistrationByDerivedOwner) { + void* owner_pointer = nullptr; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); + Object cleanup_object(0); + { + DerivedOwnerObject derived_owner; + owner_pointer = &derived_owner; + CleanupNotifier* notifier = CleanupNotifier::FindByOwner(owner_pointer); + EXPECT_TRUE(notifier != nullptr); + notifier->RegisterObject(&cleanup_object, Object::IncrementCounter); + } + EXPECT_EQ(1, cleanup_object.counter); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, + CleanupSubscriberObjectOnOwnerDeletion) { + bool subscriber_deleted = false; + OwnerObject* owner = new OwnerObject; + SubscriberObject* subscriber = + new SubscriberObject(owner, &subscriber_deleted); + (void)subscriber; + delete owner; + EXPECT_TRUE(subscriber_deleted); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, + CleanupSubscriberObjectBeforeOwnerDeletion) { + bool subscriber_deleted = false; + OwnerObject owner; + { + SubscriberObject subscriber(&owner, &subscriber_deleted); + (void)subscriber; + } + (void)owner; + EXPECT_TRUE(subscriber_deleted); +} + +class TypedCleanupNotifierTest : public ::testing::Test {}; + +namespace { +struct TypedObject { + explicit TypedObject(int counter_) : counter(counter_) {} + int counter; + + static void IncrementCounter(TypedObject* obj) { obj->counter++; } + static void DecrementCounter(TypedObject* obj) { obj->counter--; } +}; +} // namespace + +TEST_F(TypedCleanupNotifierTest, TestCallbacksAreCalledAutomatically) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestCallbacksAreCalledManuallyOnceOnly) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + } + // Ensure the callback isn't called again. + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestCallbacksCanBeUnregistered) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + cleanup.UnregisterObject(&obj); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(TypedCleanupNotifierTest, TestMultipleObjects) { + TypedObject obj1(1), obj2(2); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, TypedObject::IncrementCounter); + cleanup.RegisterObject(&obj2, TypedObject::IncrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(TypedCleanupNotifierTest, TestMultipleCallbacksMultipleObjects) { + TypedObject obj1(1), obj2(2); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, TypedObject::IncrementCounter); + cleanup.RegisterObject(&obj2, TypedObject::DecrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestOnlyOneCallbackPerObject) { + TypedObject obj1(1), obj2(2); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, TypedObject::IncrementCounter); + cleanup.RegisterObject(&obj2, TypedObject::IncrementCounter); + // The following call overwrites the previous callback on obj1: + cleanup.RegisterObject(&obj1, TypedObject::DecrementCounter); + EXPECT_EQ(obj1.counter, 1); // Has not run. + } + EXPECT_EQ(obj1.counter, 0); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(TypedCleanupNotifierTest, + TestDoesNotCrashWhenYouUnregisterInvalidObject) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + } + EXPECT_EQ(obj.counter, 0); + { + TypedCleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestDoesNotCrashIfCallingZeroCallbacks) { + TypedObject obj(0); + { TypedCleanupNotifier cleanup; } + { + TypedCleanupNotifier cleanup; + cleanup.CleanupAll(); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(TypedCleanupNotifierTest, + TestMultipleTypedCleanupNotifiersReferringToSameObject) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup1, cleanup2; + cleanup1.RegisterObject(&obj, TypedObject::IncrementCounter); + cleanup2.RegisterObject(&obj, TypedObject::IncrementCounter); + } + EXPECT_EQ(obj.counter, 2); +} + +} // namespace testing +} // namespace firebase diff --git a/app/tests/flexbuffer_matcher.cc b/app/tests/flexbuffer_matcher.cc new file mode 100644 index 0000000000..525186616d --- /dev/null +++ b/app/tests/flexbuffer_matcher.cc @@ -0,0 +1,252 @@ +// Copyright 2020 Google LLC +// +// 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 "app/tests/flexbuffer_matcher.h" + +// For testing purposes, we only care about the basic types. +enum FlexbuffersMetaTypes { + kNull, + kBool, + kInt, + kUInt, + kFloat, + kString, + kKey, + kMap, + kVector, + kBlob, +}; + +// Type names for error messages. +const char* meta_type_names[] = { + "Null", "Bool", "Int", "UInt", "Float", + "String", "Key", "Map", "Vector", "Blob", +}; + +FlexbuffersMetaTypes GetFlexbuffersReferenceType( + const flexbuffers::Reference& ref) { + switch (ref.GetType()) { + case flexbuffers::FBT_NULL: { + return kNull; + } + case flexbuffers::FBT_BOOL: { + return kBool; + } + case flexbuffers::FBT_INDIRECT_INT: + case flexbuffers::FBT_INT: { + return kInt; + } + case flexbuffers::FBT_INDIRECT_UINT: + case flexbuffers::FBT_UINT: { + return kUInt; + } + case flexbuffers::FBT_INDIRECT_FLOAT: + case flexbuffers::FBT_FLOAT: { + return kFloat; + } + case flexbuffers::FBT_KEY: { + return kKey; + } + case flexbuffers::FBT_STRING: { + return kString; + } + case flexbuffers::FBT_MAP: { + return kMap; + } + case flexbuffers::FBT_VECTOR: + case flexbuffers::FBT_VECTOR_INT: + case flexbuffers::FBT_VECTOR_UINT: + case flexbuffers::FBT_VECTOR_FLOAT: + case flexbuffers::FBT_VECTOR_KEY: + case flexbuffers::FBT_VECTOR_STRING_DEPRECATED: + case flexbuffers::FBT_VECTOR_INT2: + case flexbuffers::FBT_VECTOR_UINT2: + case flexbuffers::FBT_VECTOR_FLOAT2: + case flexbuffers::FBT_VECTOR_INT3: + case flexbuffers::FBT_VECTOR_UINT3: + case flexbuffers::FBT_VECTOR_FLOAT3: + case flexbuffers::FBT_VECTOR_INT4: + case flexbuffers::FBT_VECTOR_UINT4: + case flexbuffers::FBT_VECTOR_FLOAT4: + case flexbuffers::FBT_VECTOR_BOOL: { + return kVector; + } + case flexbuffers::FBT_BLOB: { + return kBlob; + } + } +} + +template +void MismatchMessage(const std::string& title, const T& expected, const T& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + *result_listener << title << ": Expected " << expected; + if (!location.empty()) { + *result_listener << " at " << location; + } + *result_listener << ", got " << arg; +} + +// TODO(73494146): Check in EqualsFlexbuffer gmock matcher into the canonical +// Flatbuffer repository. +// Because pushing things to the Flatbuffers library is a multistep process, I'm +// including this for now so the tests can be built. Once this has been merged +// into flatbuffers, we can remove this implementation of it and use the one +// supplied by Flatbuffers. +// +// Checks the equality of two Flexbuffers. This checker ignores whether values +// are 'Indirect' and typed vectors are treated as plain vectors. +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + FlexbuffersMetaTypes expected_type = GetFlexbuffersReferenceType(expected); + FlexbuffersMetaTypes arg_type = GetFlexbuffersReferenceType(arg); + + if (expected_type != arg_type) { + MismatchMessage("Type mismatch", meta_type_names[expected_type], + meta_type_names[arg_type], location, result_listener); + return false; + } + + if (expected.IsNull()) { + // No value checking necessary as Null has no value. + return true; + } + if (expected.IsBool()) { + if (expected.AsBool() != arg.AsBool()) { + MismatchMessage("Value mismatch", (expected.AsBool() ? "true" : "false"), + (arg.AsBool() ? "true" : "false"), location, + result_listener); + return false; + } + return true; + } else if (expected.IsInt()) { + if (expected.AsInt64() != arg.AsInt64()) { + MismatchMessage("Value mismatch", expected.AsInt64(), arg.AsInt64(), + location, result_listener); + return false; + } + return true; + } else if (expected.IsUInt()) { + if (expected.AsUInt64() != arg.AsUInt64()) { + MismatchMessage("Value mismatch", expected.AsUInt64(), arg.AsUInt64(), + location, result_listener); + return false; + } + return true; + } else if (expected.IsFloat()) { + if (expected.AsDouble() != arg.AsDouble()) { + MismatchMessage("Value mismatch", expected.AsDouble(), arg.AsDouble(), + location, result_listener); + return false; + } + return true; + } else if (expected.IsString()) { + if (strcmp(expected.AsString().c_str(), arg.AsString().c_str()) != 0) { + MismatchMessage("Value mismatch", expected.AsString().c_str(), + arg.AsString().c_str(), location, result_listener); + return false; + } + return true; + } else if (expected.IsKey()) { + if (strcmp(expected.AsKey(), arg.AsKey()) != 0) { + MismatchMessage("Key mismatch", expected.AsKey(), arg.AsKey(), location, + result_listener); + return false; + } + return true; + } else if (expected.IsBlob()) { + if (expected.AsBlob().size() != arg.AsBlob().size() | + std::memcmp(expected.AsBlob().data(), arg.AsBlob().data(), + expected.AsBlob().size()) != 0) { + *result_listener << "Binary mismatch"; + if (!location.empty()) { + *result_listener << " at " << location; + } + return false; + } + return true; + } else if (expected.IsMap()) { + flexbuffers::Map expected_map = expected.AsMap(); + flexbuffers::Map arg_map = arg.AsMap(); + if (expected_map.size() != arg_map.size()) { + MismatchMessage("Map size mismatch", + std::to_string(expected_map.size()) + " elements", + std::to_string(arg_map.size()) + " elements", location, + result_listener); + return false; + } + flexbuffers::TypedVector expected_keys = expected_map.Keys(); + flexbuffers::TypedVector arg_keys = arg_map.Keys(); + for (size_t i = 0; i < expected_keys.size(); ++i) { + std::string new_location = + location + "[" + expected_keys[i].AsKey() + "]"; + if (!EqualsFlexbufferImpl(expected_keys[i], arg_keys[i], new_location, + result_listener)) { + return false; + } + } + // Don't return in case of success, because we still need to check that + // the values match. This is done in the IsVector section, since Maps are + // also Vectors. + } + if (expected.IsVector()) { + flexbuffers::Vector expected_vector = expected.AsVector(); + flexbuffers::Vector arg_vector = arg.AsVector(); + if (expected_vector.size() != arg_vector.size()) { + MismatchMessage("Vector size mismatch", + std::to_string(expected_vector.size()) + " elements", + std::to_string(arg_vector.size()) + " elements", location, + result_listener); + return false; + } + for (size_t i = 0; i < expected_vector.size(); ++i) { + std::string new_location = location + "[" + std::to_string(i) + "]"; + if (!EqualsFlexbufferImpl(expected_vector[i], arg_vector[i], new_location, + result_listener)) { + return false; + } + } + return true; + } + *result_listener << "Unrecognized type"; + return false; +} + +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + return EqualsFlexbufferImpl(expected, flexbuffers::GetRoot(arg), location, + result_listener); +} + +bool EqualsFlexbufferImpl(const std::vector& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + return EqualsFlexbufferImpl(flexbuffers::GetRoot(expected), arg, location, + result_listener); +} + +bool EqualsFlexbufferImpl(const std::vector& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + return EqualsFlexbufferImpl(flexbuffers::GetRoot(expected), + flexbuffers::GetRoot(arg), location, + result_listener); +} diff --git a/app/tests/flexbuffer_matcher.h b/app/tests/flexbuffer_matcher.h new file mode 100644 index 0000000000..a1cb98a5cb --- /dev/null +++ b/app/tests/flexbuffer_matcher.h @@ -0,0 +1,56 @@ +// Copyright 2020 Google LLC +// +// 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. + +#ifndef FIREBASE_APP_CLIENT_CPP_TESTS_FLEXBUFFER_MATCHER_H_ +#define FIREBASE_APP_CLIENT_CPP_TESTS_FLEXBUFFER_MATCHER_H_ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "flatbuffers/flexbuffers.h" + +// TODO(73494146): Check in EqualsFlexbuffer gmock matcher into the canonical +// Flatbuffer repository. +// Because pushing things to the Flatbuffers library is a multistep process, I'm +// including this for now so the tests can be built. Once this has been merged +// into flatbuffers, we can remove this implementation of it and use the one +// supplied by Flatbuffers. +// +// Checks the equality of two Flexbuffers. This checker ignores whether values +// are 'Indirect' and typed vectors are treated as plain vectors. +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +bool EqualsFlexbufferImpl(const std::vector& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +bool EqualsFlexbufferImpl(const std::vector& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +// TODO(73494146): Move this to Flabuffers. +MATCHER_P(EqualsFlexbuffer, expected, "") { + return EqualsFlexbufferImpl(expected, arg, "", result_listener); +} + +#endif // FIREBASE_APP_CLIENT_CPP_TESTS_FLEXBUFFER_MATCHER_H_ diff --git a/app/tests/flexbuffer_matcher_test.cc b/app/tests/flexbuffer_matcher_test.cc new file mode 100644 index 0000000000..044bef89e7 --- /dev/null +++ b/app/tests/flexbuffer_matcher_test.cc @@ -0,0 +1,236 @@ +// Copyright 2020 Google LLC +// +// 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 "app/tests/flexbuffer_matcher.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "flatbuffers/flexbuffers.h" + +using ::testing::Not; + +namespace { + +class FlexbufferMatcherTest : public ::testing::Test { + protected: + FlexbufferMatcherTest() : fbb_(512) {} + + void SetUp() override { + // Null type. + fbb_.Null(); + fbb_.Finish(); + null_flexbuffer_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Bool type. + fbb_.Bool(false); + fbb_.Finish(); + bool_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Bool(true); + fbb_.Finish(); + bool_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Int type. + fbb_.Int(5); + fbb_.Finish(); + int_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Int(10); + fbb_.Finish(); + int_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // UInt type. + fbb_.UInt(100); + fbb_.Finish(); + uint_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.UInt(500); + fbb_.Finish(); + uint_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Float type. + fbb_.Float(12.5); + fbb_.Finish(); + float_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Float(100.625); + fbb_.Finish(); + float_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // String type. + fbb_.String("A sailor went to sea sea sea"); + fbb_.Finish(); + string_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.String("To see what he could see see see"); + fbb_.Finish(); + string_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Key type. + fbb_.Key("But all that he could see see see"); + fbb_.Finish(); + key_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Key("Was the bottom of the deep blue sea sea sea"); + fbb_.Finish(); + key_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Map type. + fbb_.Map([&]() { + fbb_.Add("lorem", "ipsum"); + fbb_.Add("dolor", "sit"); + }); + fbb_.Finish(); + map_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Map([&]() { + fbb_.Add("amet", "consectetur"); + fbb_.Add("adipiscing", "elit"); + }); + fbb_.Finish(); + map_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Map([&]() { + fbb_.Add("sed", "do"); + fbb_.Add("eiusmod", "tempor"); + fbb_.Add("incididunt", "ut"); + }); + fbb_.Finish(); + map_flexbuffer_c_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Vector types. + fbb_.Vector([&]() { + fbb_ += "labore"; + fbb_ += "et"; + }); + fbb_.Finish(); + vector_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Vector([&]() { + fbb_ += "dolore"; + fbb_ += "magna"; + }); + fbb_.Finish(); + vector_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Vector([&]() { + fbb_ += "aliqua"; + fbb_ += "ut"; + fbb_ += "enim"; + }); + fbb_.Finish(); + vector_flexbuffer_c_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Blob types + fbb_.Blob("abcde", 5); + fbb_.Finish(); + blob_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Blob("fghij", 5); + fbb_.Finish(); + blob_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + } + + flexbuffers::Builder fbb_; + std::vector null_flexbuffer_; + std::vector bool_flexbuffer_a_; + std::vector bool_flexbuffer_b_; + std::vector int_flexbuffer_a_; + std::vector int_flexbuffer_b_; + std::vector uint_flexbuffer_a_; + std::vector uint_flexbuffer_b_; + std::vector float_flexbuffer_a_; + std::vector float_flexbuffer_b_; + std::vector string_flexbuffer_a_; + std::vector string_flexbuffer_b_; + std::vector key_flexbuffer_a_; + std::vector key_flexbuffer_b_; + std::vector map_flexbuffer_a_; + std::vector map_flexbuffer_b_; + std::vector map_flexbuffer_c_; + std::vector vector_flexbuffer_a_; + std::vector vector_flexbuffer_b_; + std::vector vector_flexbuffer_c_; + std::vector blob_flexbuffer_a_; + std::vector blob_flexbuffer_b_; +}; + +// TODO(73494146): These tests should be moved to to the Flatbuffers repo whent +// the matcher itself is. +TEST_F(FlexbufferMatcherTest, IdentityChecking) { + EXPECT_THAT(null_flexbuffer_, EqualsFlexbuffer(null_flexbuffer_)); + EXPECT_THAT(bool_flexbuffer_a_, EqualsFlexbuffer(bool_flexbuffer_a_)); + EXPECT_THAT(int_flexbuffer_a_, EqualsFlexbuffer(int_flexbuffer_a_)); + EXPECT_THAT(uint_flexbuffer_a_, EqualsFlexbuffer(uint_flexbuffer_a_)); + EXPECT_THAT(float_flexbuffer_a_, EqualsFlexbuffer(float_flexbuffer_a_)); + EXPECT_THAT(string_flexbuffer_a_, EqualsFlexbuffer(string_flexbuffer_a_)); + EXPECT_THAT(key_flexbuffer_a_, EqualsFlexbuffer(key_flexbuffer_a_)); + EXPECT_THAT(map_flexbuffer_a_, EqualsFlexbuffer(map_flexbuffer_a_)); + EXPECT_THAT(vector_flexbuffer_a_, EqualsFlexbuffer(vector_flexbuffer_a_)); + EXPECT_THAT(blob_flexbuffer_a_, EqualsFlexbuffer(blob_flexbuffer_a_)); +} + +TEST_F(FlexbufferMatcherTest, TypeMismatch) { + EXPECT_THAT(null_flexbuffer_, Not(EqualsFlexbuffer(int_flexbuffer_b_))); + EXPECT_THAT(int_flexbuffer_a_, Not(EqualsFlexbuffer(uint_flexbuffer_b_))); + EXPECT_THAT(float_flexbuffer_a_, Not(EqualsFlexbuffer(bool_flexbuffer_b_))); + EXPECT_THAT(key_flexbuffer_a_, Not(EqualsFlexbuffer(string_flexbuffer_b_))); + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(vector_flexbuffer_b_))); +} + +TEST_F(FlexbufferMatcherTest, ValueMismatch) { + EXPECT_THAT(bool_flexbuffer_a_, Not(EqualsFlexbuffer(bool_flexbuffer_b_))); + EXPECT_THAT(int_flexbuffer_a_, Not(EqualsFlexbuffer(int_flexbuffer_b_))); + EXPECT_THAT(uint_flexbuffer_a_, Not(EqualsFlexbuffer(uint_flexbuffer_b_))); + EXPECT_THAT(float_flexbuffer_a_, Not(EqualsFlexbuffer(float_flexbuffer_b_))); + EXPECT_THAT(string_flexbuffer_a_, + Not(EqualsFlexbuffer(string_flexbuffer_b_))); + EXPECT_THAT(key_flexbuffer_a_, Not(EqualsFlexbuffer(key_flexbuffer_b_))); + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(map_flexbuffer_b_))); + EXPECT_THAT(vector_flexbuffer_a_, + Not(EqualsFlexbuffer(vector_flexbuffer_b_))); + EXPECT_THAT(blob_flexbuffer_a_, Not(EqualsFlexbuffer(blob_flexbuffer_b_))); +} + +TEST_F(FlexbufferMatcherTest, SizeMismatch) { + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(map_flexbuffer_c_))); + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(map_flexbuffer_c_))); + EXPECT_THAT(vector_flexbuffer_a_, + Not(EqualsFlexbuffer(vector_flexbuffer_c_))); + EXPECT_THAT(vector_flexbuffer_a_, + Not(EqualsFlexbuffer(vector_flexbuffer_c_))); +} + +} // namespace diff --git a/app/tests/future_manager_test.cc b/app/tests/future_manager_test.cc new file mode 100644 index 0000000000..217f373ac1 --- /dev/null +++ b/app/tests/future_manager_test.cc @@ -0,0 +1,205 @@ +/* + * Copyright 2016 Google LLC + * + * 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 "app/src/future_manager.h" + +#include + +#include +#include + +#include "app/src/include/firebase/future.h" +#include "app/src/semaphore.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "thread/fiber/fiber.h" +#include "util/random/mt_random_thread_safe.h" + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Ne; +using ::testing::NotNull; + +namespace firebase { +namespace detail { +namespace testing { + +enum FutureManagerTestFn { kTestFnOne, kTestFnCount }; + +class FutureManagerTest : public ::testing::Test { + protected: + FutureManager future_manager_; + int value1_; + int value2_; + int value3_; +}; + +typedef FutureManagerTest FutureManagerDeathTest; + +TEST_F(FutureManagerTest, TestAllocFutureApis) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + future_manager_.AllocFutureApi(&value2_, kTestFnCount); + + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), + Ne(future_manager_.GetFutureApi(&value2_))); + EXPECT_THAT(future_manager_.GetFutureApi(&value3_), IsNull()); +} + +TEST_F(FutureManagerTest, TestMoveFutureApis) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + ReferenceCountedFutureImpl* impl = future_manager_.GetFutureApi(&value1_); + future_manager_.MoveFutureApi(&value1_, &value2_); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), Eq(impl)); +} + +TEST_F(FutureManagerTest, TestReleaseFutureApi) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), NotNull()); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); +} + +TEST_F(FutureManagerTest, TestOrphaningFutures) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + EXPECT_THAT(future_impl, NotNull()); + + auto handle = future_impl->SafeAlloc(kTestFnOne); + Future future = + static_cast&>(future_impl->LastResult(kTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_impl->Complete(handle, 0, ""); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); +} + +TEST_F(FutureManagerDeathTest, TestCleanupOrphanedFuturesApis) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + EXPECT_THAT(future_impl, NotNull()); + + auto handle = future_impl->SafeAlloc(kTestFnOne); + handle.Detach(); + { + Future future = + static_cast&>(future_impl->LastResult(kTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + } + + // Future should still be valid even after cleanup since it is still pending. + future_manager_.CleanupOrphanedFutureApis(false); + EXPECT_THAT(future_impl->LastResult(kTestFnOne).status(), + Eq(kFutureStatusPending)); + + // Future should no longer be valid after cleanup since it is complete. + future_impl->Complete(handle, 0, ""); + future_manager_.CleanupOrphanedFutureApis(false); + EXPECT_DEATH(future_impl->SafeAlloc(kTestFnOne), "SIGSEGV"); +} + +TEST_F(FutureManagerDeathTest, TestCleanupOrphanedFuturesApisForcefully) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + EXPECT_THAT(future_impl, NotNull()); + + future_impl->SafeAlloc(kTestFnOne); + + { + Future future = + static_cast&>(future_impl->LastResult(kTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + } + + // Future should no longer be valid after force cleanup regardless of whether + // or not it is complete. + future_manager_.CleanupOrphanedFutureApis(true); + EXPECT_DEATH(future_impl->SafeAlloc(kTestFnOne), "SIGSEGV"); +} + +TEST_F(FutureManagerDeathTest, + TestCleanupIsNotTriggeredWhileRunningUserCallback) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + // The other future api is only allocated so that it can be released in the + // completion, triggering cleanup. + future_manager_.AllocFutureApi(&value2_, kTestFnCount); + + auto handle = future_impl->SafeAlloc(kTestFnOne); + Future future(future_impl, handle.get()); + + Semaphore semaphore(0); + future.OnCompletion([&](const Future& future) { + // Triggers cleanup of orphaned instances (calls CleanupOrphanedFutureApis + // under the hood). + future_manager_.ReleaseFutureApi(&value2_); + // The future api shouldn't have been cleaned up by the previous line. + ASSERT_NE(future.status(), kFutureStatusInvalid); + EXPECT_EQ(*future.result(), 42); + + semaphore.Post(); + }); + + future_manager_.ReleaseFutureApi(&value1_); // Make it orphaned + // The future API, even though, orphaned, should not have been deallocated, + // because there is still a pending future associated with it. + EXPECT_EQ(future.status(), kFutureStatusPending); + future_impl->CompleteWithResult(handle, 0, "", 42); + + semaphore.Wait(); +} + +} // namespace testing +} // namespace detail +} // namespace firebase diff --git a/app/tests/future_test.cc b/app/tests/future_test.cc new file mode 100644 index 0000000000..080e080b0a --- /dev/null +++ b/app/tests/future_test.cc @@ -0,0 +1,1567 @@ +/* + * Copyright 2016 Google LLC + * + * 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 "app/src/include/firebase/future.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "app/src/reference_counted_future_impl.h" +#include "app/src/semaphore.h" +#include "app/src/thread.h" +#include "app/src/time.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Ne; +using ::testing::NotNull; + +// Namespace to use to access library components under test. +#if !defined(TEST_FIREBASE_NAMESPACE) +#define TEST_FIREBASE_NAMESPACE firebase +#endif // !defined(TEST_FIREBASE_NAMESPACE) + +namespace TEST_FIREBASE_NAMESPACE { +namespace detail { +namespace testing { + +struct TestResult { + int number; + std::string text; +}; + +class FutureTest : public ::testing::Test { + protected: + enum FutureTestFn { kFutureTestFnOne, kFutureTestFnTwo, kFutureTestFnCount }; + + FutureTest() : future_impl_(kFutureTestFnCount) {} + void SetUp() override { + handle_ = future_impl_.SafeAlloc(); + future_ = MakeFuture(&future_impl_, handle_); + } + + public: + ReferenceCountedFutureImpl future_impl_; + SafeFutureHandle handle_; + Future future_; +}; + +// Some arbitrary result and error values. +const int kResultNumber = 8675309; +const int kResultError = -1729; +const char* const kResultText = "Hello, world!"; + +// Check that a future can be completed by the same thread. +TEST_F(FutureTest, TestFutureCompletesInSameThread) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +static void FutureCallback(TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; +} + +// Check that the future completion can be done with a callback function +// instead of a lambda. +TEST_F(FutureTest, TestFutureCompletesWithCallback) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, FutureCallback); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that the LastResult() futures are properly set and completed. +TEST_F(FutureTest, TestLastResult) { + const auto handle = future_impl_.SafeAlloc(kFutureTestFnOne); + + Future future = static_cast&>( + future_impl_.LastResult(kFutureTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle, 0); + + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); +} + +// Check that CompleteWithResult() works (i.e. data copy instead of lambda). +TEST_F(FutureTest, TestCompleteWithCopy) { + TestResult result; + result.number = kResultNumber; + result.text = kResultText; + future_impl_.CompleteWithResult(handle_, 0, result); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that Complete() with a lambda works. +TEST_F(FutureTest, TestCompleteWithLambda) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that Complete() with a lambda with a capture works. +TEST_F(FutureTest, TestCompleteWithLambdaCapture) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + bool captured = true; + future_impl_.Complete(handle_, 0, [&captured](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + captured = true; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + EXPECT_THAT(captured, Eq(true)); +} + +// Test that the result of a Pending future is null. +TEST_F(FutureTest, TestPendingResultIsNull) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_.result(), IsNull()); + EXPECT_THAT(future_.result_void(), IsNull()); +} + +// Check that a future can be completed from another thread. +TEST_F(FutureTest, TestFutureCompletesInAnotherThread) { + Thread child( + [](void* test_void) { + FutureTest* test = static_cast(test_void); + test->future_impl_.Complete(test->handle_, 0, + [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + }, + this); + child.Join(); // Blocks until the thread function is done + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that the future error can be set. +TEST_F(FutureTest, TestSettingErrorValue) { + future_impl_.Complete(handle_, kResultError); + EXPECT_THAT(future_.error(), Eq(kResultError)); +} + +// Check that the void and typed results match. +TEST_F(FutureTest, TestTypedAndVoidMatch) { + future_impl_.Complete(handle_, kResultError); + + EXPECT_THAT(future_.result(), NotNull()); + EXPECT_THAT(future_.result_void(), NotNull()); + EXPECT_THAT(future_.result(), Eq(future_.result_void())); +} + +TEST_F(FutureTest, TestReleasedBackingData) { + FutureHandleId id; + { + Future future; + { + SafeFutureHandle handle = + future_impl_.SafeAlloc(); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + id = handle.get().id(); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + future = MakeFuture(&future_impl_, handle); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + } + EXPECT_TRUE(future_impl_.ValidFuture(id)); + } + EXPECT_FALSE(future_impl_.ValidFuture(id)); +} + +TEST_F(FutureTest, TestDetachFutureHandle) { + FutureHandleId id; + { + Future future; + SafeFutureHandle handle = future_impl_.SafeAlloc(); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + id = handle.get().id(); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + future = MakeFuture(&future_impl_, handle); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + future = Future(); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + handle.Detach(); + EXPECT_FALSE(future_impl_.ValidFuture(handle)); + EXPECT_FALSE(future_impl_.ValidFuture(id)); + } + EXPECT_FALSE(future_impl_.ValidFuture(id)); +} + +// Test that a future becomes invalid when you release it. +TEST_F(FutureTest, TestReleasedFutureGoesInvalid) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + future_.Release(); + EXPECT_THAT(future_.status(), Eq(kFutureStatusInvalid)); +} + +// Test that an invalid future returns an error. +TEST_F(FutureTest, TestReleasedFutureHasError) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + future_.Release(); + EXPECT_THAT(future_.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_.error(), Ne(0)); +} + +TEST_F(FutureTest, TestCompleteSetsStatusToComplete) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Can't mock a simple function pointer, so we use these globals to ensure +// expectations about the callback running. +static int g_callback_times_called = -99; +static int g_callback_result_number = -99; +static void* g_callback_user_data = nullptr; + +// Test whether an OnCompletion callback is called when the future is completed +// with the templated version of Complete(). +TEST_F(FutureTest, TestCallbackCalledWhenSettingResult) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with the templated version of Complete(). +TEST_F(FutureTest, TestAddCallbackCalledWhenSettingResult) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an OnCompletion callback is called when the future is completed +// with a lambda with a capture. +TEST_F(FutureTest, TestCallbackCalledWithTypedLambdaCapture) { + int callback_times_called = 0; + int callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion([&](const Future& result) { + callback_times_called++; + callback_result_number = result.result()->number; + }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(callback_times_called, Eq(1)); + EXPECT_THAT(callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with a lambda with a capture. +TEST_F(FutureTest, TestAddCallbackCalledWithTypedLambdaCapture) { + int callback_times_called = 0; + int callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion([&](const Future& result) { + callback_times_called++; + callback_result_number = result.result()->number; + }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(callback_times_called, Eq(1)); + EXPECT_THAT(callback_result_number, Eq(kResultNumber)); +} + +// Test whether an OnCompletion callback is called when the future is completed +// with a lambda with a capture. +TEST_F(FutureTest, TestCallbackCalledWithBaseLambdaCapture) { + int callback_times_called = 0; + + // Set the callback before setting the status to complete. + static_cast(future_).OnCompletion( + [&](const FutureBase& result) { callback_times_called++; }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(callback_times_called, Eq(1)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with a lambda with a capture. +TEST_F(FutureTest, TestAddCallbackCalledWithBaseLambdaCapture) { + int callback_times_called = 0; + + // Set the callback before setting the status to complete. + static_cast(future_).AddOnCompletion( + [&](const FutureBase& result) { callback_times_called++; }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(callback_times_called, Eq(1)); +} + +void OnCompletionCallback(const Future& result, + void* /*user_data*/) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; +} + +// Test whether an OnCompletion callback is called when the callback is a +// function pointer instead of a lambda. +TEST_F(FutureTest, TestCallbackCalledWhenFunctionPointer) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion(OnCompletionCallback, nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called when the callback is a +// function pointer instead of a lambda. +TEST_F(FutureTest, TestAddCallbackCalledWhenFunctionPointer) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion(OnCompletionCallback, nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an OnCompletion callback is called when the future is completed +// with the non-templated version of Complete(). +TEST_F(FutureTest, TestCallbackCalledWhenNotSettingResults) { + g_callback_times_called = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with the non-templated version of Complete(). +TEST_F(FutureTest, TestAddCallbackCalledWhenNotSettingResults) { + g_callback_times_called = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); +} + +// Test whether an OnCompletion callback is called even if the future was +// already completed before OnCompletion() was called. +TEST_F(FutureTest, TestCallbackCalledWhenAlreadyComplete) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + // Callback should not be called until the callback is set. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + // Set the callback *after* the future was already completed. + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called even if the future was +// already completed before AddOnCompletion() was set. +TEST_F(FutureTest, TestAddCallbackCalledWhenAlreadyComplete) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + // Callback should not be called until the callback is set. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + // Set the callback *after* the future was already completed. + future_.AddOnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestCallbackCalledFromAnotherThread) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + Thread child( + [](void* test_void) { + FutureTest* test = static_cast(test_void); + test->future_impl_.Complete( + test->handle_, 0, + [](TestResult* data) { data->number = kResultNumber; }); + }, + this); + + child.Join(); // Blocks until the thread function is done + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestAddCallbackCalledFromAnotherThread) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_.AddOnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + Thread child( + [](void* test_void) { + FutureTest* test = static_cast(test_void); + test->future_impl_.Complete( + test->handle_, 0, + [](TestResult* data) { data->number = kResultNumber; }); + }, + this); + + child.Join(); // Blocks until the fiber function is done + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestCallbackUserData) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + future_.OnCompletion( + [](const Future&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestAddCallbackUserData) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + future_.AddOnCompletion( + [](const Future&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestCallbackUserDataFromBaseClass) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + static_cast(future_).OnCompletion( + [](const FutureBase&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestAddCallbackUserDataFromBaseClass) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + static_cast(future_).AddOnCompletion( + [](const FutureBase&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestUntypedCallback) { + g_callback_times_called = 0; + g_callback_result_number = 0; + static_cast(future_).OnCompletion( + [](const FutureBase& untyped_result, void*) { + g_callback_times_called++; + const Future& typed_result = + reinterpret_cast&>(untyped_result); + g_callback_result_number = typed_result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestAddUntypedCallback) { + g_callback_times_called = 0; + g_callback_result_number = 0; + static_cast(future_).AddOnCompletion( + [](const FutureBase& untyped_result, void*) { + g_callback_times_called++; + const Future& typed_result = + reinterpret_cast&>(untyped_result); + g_callback_result_number = typed_result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test that you can deal with many simultaneous Futures at once. +TEST_F(FutureTest, TestSimultaneousFutures) { + const int kMilliseconds = 1000; + const int kNumToTest = 100; + + // Initialize a bunch of futures and threads. + std::vector> handles_; + std::vector> futures_; + std::vector children_; + struct Context { + FutureTest* test; + SafeFutureHandle handle; + int test_number; + } thread_context[kNumToTest]; + for (int i = 0; i < kNumToTest; i++) { + auto handle = future_impl_.SafeAlloc(); + handles_.push_back(handle); + futures_.push_back(MakeFuture(&future_impl_, handle)); + auto* context = &thread_context[i]; + context->test = this; + context->handle = handle; + context->test_number = i; + children_.push_back(new Thread( + [](void* current_context_void) { + Context* current_context = + static_cast(current_context_void); + // Each thread should wait a moment, then set the result and + // complete. + internal::Sleep(rand() % kMilliseconds); // NOLINT + current_context->test->future_impl_.Complete( + current_context->handle, 0, [current_context](TestResult* data) { + data->number = kResultNumber + current_context->test_number; + }); + }, + context)); + } + // Give threads time to run. + internal::Sleep(kMilliseconds); + + // Check that each future completed successfully, then clean it up. + for (int i = 0; i < kNumToTest; i++) { + children_[i]->Join(); + EXPECT_THAT(futures_[i].result()->number, Eq(kResultNumber + i)); + delete children_[i]; + children_[i] = nullptr; + } +} + +TEST_F(FutureTest, TestCallbackOnFutureOutOfScope) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_ = Future(); + handle_.Detach(); + // The Future we were holding onto is now out of scope. + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestOverridingHandle) { + // Ensure that FutureHandles can't be deallocated while still in use. + // Generally, do this by allocating a handle into a function slot, then + // allocating another handle into the same slot, and then creating a future + // from the first handle. If all goes well it should be fine, but if the + // handle was deallocated then making a future from it will fail. + + { + // Basic test, create 2 FutureHandles in the same slot, then make Future + // instances from both. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusPending); + } + { + // Same as above, but complete the first Future and make sure it doesn't + // affect the second. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + future_impl_.Complete( + handle1, 0, [](TestResult* data) { data->number = kResultNumber; }); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusComplete); + EXPECT_EQ(future1.result()->number, kResultNumber); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusPending); + } + { + // Complete the second Future and make sure it doesn't affect the first. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + future_impl_.Complete( + handle2, 0, [](TestResult* data) { data->number = kResultNumber; }); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusComplete); + EXPECT_EQ(future2.result()->number, kResultNumber); + } + { + // Ensure that both Futures can be completed with different result values. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + future_impl_.Complete( + handle1, 0, [](TestResult* data) { data->number = kResultNumber; }); + future_impl_.Complete( + handle2, 0, [](TestResult* data) { data->number = 2 * kResultNumber; }); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusComplete); + EXPECT_EQ(future1.result()->number, kResultNumber); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusComplete); + EXPECT_EQ(future2.result()->number, 2 * kResultNumber); + } +} + +TEST_F(FutureTest, TestHighQps) { + const int kNumToTest = 10000; + + future_ = Future(); + + std::vector children_; + for (int i = 0; i < kNumToTest; i++) { + children_.push_back(new Thread( + [](void* this_void) { + FutureTest* this_ = reinterpret_cast(this_void); + SafeFutureHandle handle = + this_->future_impl_.SafeAlloc(kFutureTestFnOne); + + this_->future_impl_.Complete( + handle, 0, + [](TestResult* data) { data->number = kResultNumber; }); + Future future = MakeFuture(&this_->future_impl_, handle); + }, + this)); + } + for (int i = 0; i < kNumToTest; i++) { + children_[i]->Join(); + delete children_[i]; + children_[i] = nullptr; + } +} + +// Test that accessing a future as const compiles. +TEST_F(FutureTest, TestConstFuture) { + g_callback_times_called = 0; + + const Future const_future = future_; + // Set the callback before setting the status to complete. + const_future.OnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + const_future.AddOnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(2)); +} + +// Test that we can remove an AddOnCompletion callback using RemoveOnCompletion. +TEST_F(FutureTest, TestAddCompletionCallbackRemoval) { + g_callback_times_called = 0; + auto callback_handle = future_.AddOnCompletion( + [&](const Future&) { ++g_callback_times_called; }); + future_.RemoveOnCompletion(callback_handle); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(0)); +} + +// Test that multiple callbacks are called in the documented order, +// and that OnCompletion() doesn't interfere with AddOnCompletion() +// and vice versa. +TEST_F(FutureTest, TestCallbackOrdering) { + std::vector ordered_results; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(5); }); + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(4); }); + auto callback_handle = future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(3); }); + future_.OnCompletion( + [&](const Future&) { ordered_results.push_back(-3); }); + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(2); }); + future_.OnCompletion( + [&](const Future&) { ordered_results.push_back(-2); }); + future_.OnCompletion( + [&](const Future&) { ordered_results.push_back(-1); }); + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(1); }); + future_.RemoveOnCompletion(callback_handle); + + // Callback should not be called until it is completed. + EXPECT_THAT(ordered_results, Eq(std::vector{})); + + future_impl_.Complete(handle_, 0); + + // The last OnCompletionCallback (-1) should get called before AddOnCompletion + // callbacks, and the AddOnCompletion callbacks should get called in + // the order that they were registered (5, 4, 3, 2, 1), except that callbacks + // which have been removed (3) should not be called. + EXPECT_THAT(ordered_results, Eq(std::vector{-1, 5, 4, 2, 1})); +} + +// Verify futures are not leaked when copied, using the implicit memory leak +// checker. When futures are allocated in the same LastResult function slot, a +// new handle should be allocated the old handle should be removed and hence be +// invalid. +TEST_F(FutureTest, VerifyNotLeakedWhenOverridden) { + FutureHandleId id; + { + SafeFutureHandle last_result_handle; + last_result_handle = future_impl_.SafeAlloc(0); + EXPECT_THAT(last_result_handle.get(), + Ne(SafeFutureHandle::kInvalidHandle.get())); + EXPECT_TRUE(future_impl_.ValidFuture(last_result_handle)); + id = last_result_handle.get().id(); + } + { + auto new_last_result_handle = future_impl_.SafeAlloc(0); + EXPECT_THAT(new_last_result_handle.get(), + Ne(SafeFutureHandle::kInvalidHandle.get())); + EXPECT_FALSE(future_impl_.ValidFuture(id)); + } +} + +// Verify that trying to complete a future twice causes death. +TEST_F(FutureTest, VerifyCompletingFutureTwiceAsserts) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0); + EXPECT_DEATH(future_impl_.Complete(handle_, 0), "SIGABRT"); +} + +// Verify that IsSafeToDelete() return the correct value. +TEST_F(FutureTest, VerifyIsSafeToDelete) { + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated but no external Future has ever + // reference it. + // Note: This will result in a warning message "Future with handle x still + // exists though its backing API y is being deleted" because there is no + // chance to remove the backing at all. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + auto handle_pending = impl.SafeAlloc(); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle_pending, 0); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated and an external Future has referenced + // it. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + auto handle_complete = impl.SafeAlloc(); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future* future = + new Future(&impl, handle_complete.get()); + EXPECT_FALSE(impl.IsSafeToDelete()); + delete future; + } + // This is true because ReferenceCountedFutureImpl::last_results_ never + // keeps a copy of this future. That is, the backing will be deleted when + // the future above is deleted. + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated with function id but no external + // Future has ever reference it. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + auto handle_fn_pending = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle_fn_pending, 0); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated with function id and an external Future + // has referenced it. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + auto handle_fn_complete = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future* future = + new Future(&impl, handle_fn_complete.get()); + EXPECT_FALSE(impl.IsSafeToDelete()); + delete future; + // This is false because ReferenceCountedFutureImpl::last_results_ keeps + // a copy of this future. + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle_fn_complete, 0); + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test that a ReferenceCountedFutureImpl isn't considered for deletion while + // it's running a user callback. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + auto handle = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future future = MakeFuture(&impl, handle); + EXPECT_FALSE(impl.IsSafeToDelete()); + + Semaphore semaphore(0); + future.OnCompletion([&](const Future& future) { + EXPECT_FALSE(impl.IsSafeToDelete()); // Because the callback is running. + semaphore.Post(); + }); + future.Release(); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle, 0, ""); + + semaphore.Wait(); + + // Note: despite the semaphore, the check for `impl.IsSafeToDelete` is racy + // (it could be false if the check happens in-between when the semaphore + // posts the signal and when user callback actually finishes running), which + // necessitates sleeping. + const int kSleepTimeMs = 50; + int timeout_left = 1000; + while (!impl.IsSafeToDelete() && timeout_left >= 0) { + timeout_left -= kSleepTimeMs; + internal::Sleep(kSleepTimeMs); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Like the test above, but with AddOnCompletion instead of OnCompletion. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + auto handle = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future future = MakeFuture(&impl, handle); + EXPECT_FALSE(impl.IsSafeToDelete()); + + Semaphore semaphore(0); + future.AddOnCompletion([&](const Future& future) { + EXPECT_FALSE(impl.IsSafeToDelete()); // Because the callback is running. + semaphore.Post(); + }); + future.Release(); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle, 0, ""); + + semaphore.Wait(); + + // Note: despite the semaphore, the check for `impl.IsSafeToDelete` is racy + // (it could be false if the check happens in-between when the semaphore + // posts the signal and when user callback actually finishes running), which + // necessitates sleeping. + const int kSleepTimeMs = 50; + int timeout_left = 1000; + while (!impl.IsSafeToDelete() && timeout_left >= 0) { + timeout_left -= kSleepTimeMs; + internal::Sleep(kSleepTimeMs); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } +} + +// Verify that IsReferencedExternally() returns the correct value. +TEST_F(FutureTest, VerifyIsReferencedExternally) { + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + EXPECT_FALSE(impl.IsReferencedExternally()); + } + + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + EXPECT_FALSE(impl.IsReferencedExternally()); + auto handle = impl.SafeAlloc(); + EXPECT_TRUE(impl.IsReferencedExternally()); + Future* future = new Future(&impl, handle.get()); + EXPECT_TRUE(impl.IsReferencedExternally()); + delete future; + } + EXPECT_FALSE(impl.IsReferencedExternally()); + } + + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + EXPECT_FALSE(impl.IsReferencedExternally()); + auto handle = impl.SafeAlloc(); + EXPECT_TRUE(impl.IsReferencedExternally()); + Future* future = new Future(&impl, handle.get()); + EXPECT_TRUE(impl.IsReferencedExternally()); + impl.Complete(handle, 0); + EXPECT_TRUE(impl.IsReferencedExternally()); + delete future; + } + EXPECT_FALSE(impl.IsReferencedExternally()); + } + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + EXPECT_FALSE(impl.IsReferencedExternally()); + auto handle = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_TRUE(impl.IsReferencedExternally()); + { + Future* future = + new Future(&impl, handle.get()); + delete future; + } + EXPECT_TRUE(impl.IsReferencedExternally()); + handle.Detach(); + EXPECT_FALSE(impl.IsReferencedExternally()); + } + EXPECT_FALSE(impl.IsReferencedExternally()); + } +} + +// Verify that when a ReferenceCountedFutureImpl is deleted, any +// Futures it gave out are invalidated (rather than crashing). +TEST_F(FutureTest, VerifyFutureInvalidatedWhenImplIsDeleted) { + Future future_pending, future_complete, future_fn_pending, + future_fn_complete, future_invalid; + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + // Allocate a variety of futures, completing some of them. + SafeFutureHandle handle_pending, handle_complete, + handle_fn_pending, handle_fn_complete; + + handle_pending = impl.SafeAlloc(); + future_pending = MakeFuture(&impl, handle_pending); + + handle_complete = impl.SafeAlloc(); + future_complete = MakeFuture(&impl, handle_complete); + impl.Complete(handle_complete, 0); + + handle_fn_pending = impl.SafeAlloc(kFutureTestFnOne); + future_fn_pending = MakeFuture(&impl, handle_fn_pending); + + handle_fn_complete = impl.SafeAlloc(kFutureTestFnTwo); + future_fn_complete = MakeFuture(&impl, handle_fn_complete); + impl.Complete(handle_fn_complete, 0); + + EXPECT_THAT(future_invalid.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_pending.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_complete.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_fn_pending.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_fn_complete.status(), Eq(kFutureStatusComplete)); + } + // Ensure that all different types/statuses of future are now invalid. + EXPECT_THAT(future_invalid.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_pending.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_complete.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_fn_pending.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_fn_complete.status(), Eq(kFutureStatusInvalid)); +} + +// Verify that Future instances are cleaned up properly even if they've +// been copied and moved, even between FutureImpls, or released. +TEST_F(FutureTest, TestCleaningUpFuturesThatWereCopied) { + Future future1, future2, future3; + Future copy, move, release; + Future move_c, copy_c; // Constructor versions. + { + ReferenceCountedFutureImpl impl_a(kFutureTestFnCount); + { + ReferenceCountedFutureImpl impl_b(kFutureTestFnCount); + // Allocate a variety of futures, completing some of them. + SafeFutureHandle handle1, handle2, handle3; + + handle1 = impl_a.SafeAlloc(); + future1 = MakeFuture(&impl_a, handle1); + + handle2 = impl_a.SafeAlloc(); + future2 = MakeFuture(&impl_a, handle2); + + handle3 = impl_b.SafeAlloc(); + future3 = MakeFuture(&impl_b, handle3); + + EXPECT_THAT(future1.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future3.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(move.status(), Eq(kFutureStatusInvalid)); + + // Make some copies/moves. + copy = future3; + move = std::move(future3); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future3.status(), Eq(kFutureStatusInvalid)); // NOLINT + + future1 = copy; + future2 = move; // actually a copy + EXPECT_THAT(future1.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusPending)); + + release = copy; + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(release.status(), Eq(kFutureStatusPending)); + + release.Release(); + EXPECT_THAT(future1.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(release.status(), Eq(kFutureStatusInvalid)); + + // Ensure that the move/copy constructors also work. + Future move_constructor(std::move(move)); + Future copy_constructor(copy); + EXPECT_THAT(copy_constructor.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move_constructor.status(), Eq(kFutureStatusPending)); + + move_c = std::move(move_constructor); + copy_c = copy_constructor; + EXPECT_THAT(copy_c.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy_constructor.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move_c.status(), Eq(kFutureStatusPending)); + } + // Ensure that all Futures are now invalid. + EXPECT_THAT(future1.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future3.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(move.status(), Eq(kFutureStatusInvalid)); // NOLINT + EXPECT_THAT(copy_c.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(move_c.status(), Eq(kFutureStatusInvalid)); + } +} + +// Test Wait() method (without callback), with infinite timeout. +TEST_F(FutureTest, TestFutureWaitInfinite) { + Semaphore semaphore(0); + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + future_.Wait(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + child.Join(); // Clean up. +} + +// Test Wait() method with callback, with infinite timeout. +TEST_F(FutureTest, TestFutureWaitWithCallback) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + future_.OnCompletion( + [](const Future&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + Semaphore semaphore(0); + + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + future_.Wait(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); + + child.Join(); // Clean up. +} + +// Test Wait() method with lambda callback. +TEST_F(FutureTest, TestFutureWaitWithCallbackLambda) { + int callback_times_called = 0; + future_.OnCompletion( + [&](const Future&) { callback_times_called++; }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + Semaphore semaphore(0); + + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + future_.Wait(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + EXPECT_THAT(callback_times_called, Eq(1)); + + child.Join(); // Clean up. +} + +// Test Await() method, with infinite timeout. +TEST_F(FutureTest, TestFutureAwait) { + Semaphore semaphore(0); + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + const TestResult* result = future_.Await(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + ASSERT_THAT(result, Ne(nullptr)); + EXPECT_THAT(result->number, Eq(kResultNumber)); + EXPECT_THAT(result->text, Eq(kResultText)); + + child.Join(); // Clean up. +} + +// Test Await() method, with finite timeout. +TEST_F(FutureTest, TestFutureTimedAwait) { + using This = decltype(this); + Thread child( + [](This test_fixture) { + internal::Sleep(/*milliseconds=*/300); + test_fixture->future_impl_.Complete( + test_fixture->handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + }, + this); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_.result(), Eq(nullptr)); + + const TestResult* result = future_.Await(100); // Wait for 100ms. + + // Thread should not have completed yet, for another 200ms... + EXPECT_THAT(result, Eq(nullptr)); + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + result = future_.Await(500); // Wait for 500ms. + + // Thread should have completed by now. + ASSERT_THAT(result, Ne(nullptr)); + EXPECT_THAT(result->number, Eq(kResultNumber)); + EXPECT_THAT(result->text, Eq(kResultText)); + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + child.Join(); // Clean up. +} + +// Helper functions to get memory usage. Linux only. +namespace { +extern "C" int get_memory_used_kb() { + int result = -1; +#ifdef __linux__ + FILE* file = fopen("/proc/self/status", "r"); + char line[128]; + + while (fgets(line, sizeof(line), file) != nullptr) { + if (strncmp(line, "VmSize:", 7) == 0) { + const char* nchar = &line[strlen(line) - 1]; + bool got_num = false; + while (nchar >= line) { + if (!got_num) { + if (isdigit(*nchar)) { + got_num = true; + } + } else { + if (!isdigit(*nchar)) { + result = atoi(nchar); // NOLINT + break; + } + } + nchar--; + } + } + } + fclose(file); +#endif // __linux__ + return result; +} +} // namespace + +TEST_F(FutureTest, MemoryStressTest) { + size_t kIterations = 4000000; // 4 million + + int memory_usage_before = get_memory_used_kb(); + for (size_t i = 0; i < kIterations; ++i) { + { + SafeFutureHandle handle = + i % 2 == 0 ? future_impl_.SafeAlloc() + : future_impl_.SafeAlloc(kFutureTestFnOne); + { + Future future = MakeFuture(&future_impl_, handle); + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + } + + if (i % 2 != 0) { + FutureBase future = future_impl_.LastResult(kFutureTestFnOne); + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + } + future_impl_.Complete(handle, 0, [i](TestResult* data) { + data->number = kResultNumber + i; + data->text = kResultText; + }); + { + Future future = MakeFuture(&future_impl_, handle); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.result()->number, Eq(kResultNumber + i)); + EXPECT_THAT(future.result()->text, Eq(kResultText)); + } + } + if (i % 2 != 0) { + FutureBase future_base = future_impl_.LastResult(kFutureTestFnOne); + Future& future = + *static_cast*>(&future_base); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.result()->number, Eq(kResultNumber + i)); + EXPECT_THAT(future.result()->text, Eq(kResultText)); + } + } + int memory_usage_after = get_memory_used_kb(); + + if (memory_usage_before != -1 && memory_usage_after != -1) { + // Ensure that after creating a few million futures, memory usage has not + // changed by more than half a megabyte. + const int kMaxAllowedMemoryChange = 512; // in kilobytes + EXPECT_NEAR(memory_usage_before, memory_usage_after, + kMaxAllowedMemoryChange); + } +} + +} // namespace testing +} // namespace detail +} // namespace TEST_FIREBASE_NAMESPACE diff --git a/app/tests/google_play_services/availability_android_test.cc b/app/tests/google_play_services/availability_android_test.cc new file mode 100644 index 0000000000..37119cf484 --- /dev/null +++ b/app/tests/google_play_services/availability_android_test.cc @@ -0,0 +1,242 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "absl/strings/str_format.h" + +#if !defined(__ANDROID__) +// We need enum definition in the header, which is only available for android. +// However, we cannot compile the entire test for android due to build error in +// portable //base library. +#define __ANDROID__ +#include "app/src/google_play_services/availability_android.h" +#undef __ANDROID__ +#endif // !defined(__ANDROID__) + +#include "base/stringprintf.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/run_all_tests.h" +#include "testing/testdata_config_generated.h" +#include "testing/ticker.h" + +namespace google_play_services { + +// Wait for a future up to the specified number of milliseconds. +template +static void WaitForFutureWithTimeout( + const firebase::Future& future, + int timeout_milliseconds = 1000 /* 1 second */, + firebase::FutureStatus expected_status = firebase::kFutureStatusComplete) { + while (future.status() != expected_status && timeout_milliseconds-- > 0) { + usleep(1000 /* microseconds per millisecond */); + } +} + +TEST(AvailabilityAndroidTest, Initialize) { + // Initialization should succeed. + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // Clean up afterwards. + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, InitializeTwice) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + // Should be fine if called again. + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // Terminate needs to be called twice to properly clean up. + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, CheckAvailabilityOther) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // Get null from getInstance(). Result is unavailable (other). + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailability.getInstance'}" + " ]" + "}"); + EXPECT_EQ(kAvailabilityUnavailableOther, + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // We do not care about result 10 and specify it as other. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:10}}" + " ]" + "}"); + EXPECT_EQ(kAvailabilityUnavailableOther, + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, CheckAvailabilityCases) { + // Enums are defined in com.google.android.gms.common.ConnectionResult. + const int kTestData[] = { + 0, // SUCCESS + 1, // SERVICE_MISSING + 2, // SERVICE_VERSION_UPDATE_REQUIRED + 3, // SERVICE_DISABLED + 9, // SERVICE_INVALID + 18, // SERVICE_UPDATING + 19 // SERVICE_MISSING_PERMISSION + }; + const Availability kExpected[7] = {kAvailabilityAvailable, + kAvailabilityUnavailableMissing, + kAvailabilityUnavailableUpdateRequired, + kAvailabilityUnavailableDisabled, + kAvailabilityUnavailableInvalid, + kAvailabilityUnavailableUpdating, + kAvailabilityUnavailablePermissions}; + // Now test each of the specific status. + for (int i = 0; i < sizeof(kTestData) / sizeof(kTestData[0]); ++i) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + std::string testdata = absl::StrFormat( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:%d}}" + " ]" + "}", + kTestData[i]); + firebase::testing::cppsdk::ConfigSet(testdata.c_str()); + EXPECT_EQ(kExpected[i], + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); + } +} + +TEST(AvailabilityAndroidTest, CheckAvailabilityCached) { + const int kTestData[] = { + 0, // SUCCESS + 1, // SERVICE_MISSING + 2, // SERVICE_VERSION_UPDATE_REQUIRED + }; + const Availability kExpected = kAvailabilityAvailable; + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + for (int i = 0; i < sizeof(kTestData) / sizeof(kTestData[0]); ++i) { + std::string testdata = absl::StrFormat( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:%d}}" + " ]" + "}", + kTestData[i]); + firebase::testing::cppsdk::ConfigSet(testdata.c_str()); + EXPECT_EQ(kExpected, + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, MakeAvailableAlreadyAvailable) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + // Google play services are already available. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable'," + " futurebool:{value:True}, futureint:{value:0, ticker:0}}" + " ]" + "}"); + { + firebase::Future result = MakeAvailable( + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity()); + WaitForFutureWithTimeout(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(0, result.error()); + EXPECT_STREQ("result code is 0", result.error_message()); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, MakeAvailableFailed) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + // We cannot make Google play services available. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable'," + " futurebool:{value:False}, futureint:{value:0, ticker:-1}}" + " ]" + "}"); + { + firebase::Future result = MakeAvailable( + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity()); + WaitForFutureWithTimeout(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(-1, result.error()); + EXPECT_STREQ("Call to makeGooglePlayServicesAvailable failed.", + result.error_message()); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, MakeAvailableWithStatus) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + firebase::testing::cppsdk::TickerReset(); + // We try to make Google play services available. The only difference between + // succeeded status and failed status is the result code. The logic is in the + // java helper code and transparent to the C++ code. So here we use an + // arbitrary status code 7 instead of testing each one by one. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable'," + " futurebool:{value:True}, futureint:{value:7, ticker:1}}" + " ]" + "}"); + { + firebase::Future result = MakeAvailable( + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity()); + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); + WaitForFutureWithTimeout(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(7, result.error()); + EXPECT_STREQ("result code is 7", result.error_message()); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +} // namespace google_play_services diff --git a/app/tests/google_services_test.cc b/app/tests/google_services_test.cc new file mode 100644 index 0000000000..4707278329 --- /dev/null +++ b/app/tests/google_services_test.cc @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/google_services_resource.h" +#include "app/src/log.h" +#include "flatbuffers/idl.h" +#include "flatbuffers/util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace fbs { + +// Helper function to parse config and return whether the config is valid. +bool Parse(const char* config) { + flatbuffers::IDLOptions options; + options.skip_unexpected_fields_in_json = true; + flatbuffers::Parser parser(options); + + // Parse schema. + const char* schema = + reinterpret_cast(google_services_resource_data); + if (!parser.Parse(schema)) { + ::firebase::LogError("Failed to parse schema: ", parser.error_.c_str()); + return false; + } + + // Parse actual config. + if (!parser.Parse(config)) { + ::firebase::LogError("Invalid JSON: ", parser.error_.c_str()); + return false; + } + + return true; +} + +// Test the conformity of the provided .json file. +TEST(GoogleServicesTest, TestConformity) { + // This is an actual .json, copied from Firebase auth sample app. + std::string json_file = + FLAGS_test_srcdir + + "/google3/firebase/app/client/cpp/testdata/google-services.json"; + std::string json_str; + EXPECT_TRUE(flatbuffers::LoadFile(json_file.c_str(), false, &json_str)); + EXPECT_FALSE(json_str.empty()); + EXPECT_TRUE(Parse(json_str.c_str())); +} + +// Sanity check to parse a non-conform config. +TEST(GoogleServicesTest, TestNonConformity) { + EXPECT_FALSE(Parse("{project_info:[1, 2, 3]}")); +} + +// Test that extra field in .json is ok. +TEST(GoogleServicesTest, TestExtraField) { + EXPECT_TRUE(Parse("{game_version:3.1415926}")); +} + +} // namespace fbs +} // namespace firebase diff --git a/app/tests/include/firebase/app_for_testing.h b/app/tests/include/firebase/app_for_testing.h new file mode 100644 index 0000000000..bf4e301cd0 --- /dev/null +++ b/app/tests/include/firebase/app_for_testing.h @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#ifndef FIREBASE_APP_CLIENT_CPP_TESTS_INCLUDE_FIREBASE_APP_FOR_TESTING_H_ +#define FIREBASE_APP_CLIENT_CPP_TESTS_INCLUDE_FIREBASE_APP_FOR_TESTING_H_ + +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "testing/run_all_tests.h" + +namespace firebase { +namespace testing { + +// Populate AppOptions with mock required values for testing. +static AppOptions MockAppOptions() { + AppOptions options; + options.set_app_id("com.google.firebase.testing"); + options.set_api_key("not_a_real_api_key"); + options.set_project_id("not_a_real_project_id"); + return options; +} + +// Create a named firebase::App with the specified options. +static App* CreateApp(const AppOptions& options, const char* name) { + return App::Create(options, name +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + , + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + ); +} + +// Create a default firebase::App with the specified options. +static App* CreateApp(const AppOptions& options) { + return CreateApp(options, firebase::kDefaultAppName); +} + +// Create a firebase::App with mock options. +static App* CreateApp() { return CreateApp(MockAppOptions()); } + +} // namespace testing +} // namespace firebase + +#endif // FIREBASE_APP_CLIENT_CPP_TESTS_INCLUDE_FIREBASE_APP_FOR_TESTING_H_ diff --git a/app/tests/intrusive_list_test.cc b/app/tests/intrusive_list_test.cc new file mode 100644 index 0000000000..7abd361402 --- /dev/null +++ b/app/tests/intrusive_list_test.cc @@ -0,0 +1,1229 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// 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 "app/src/intrusive_list.h" + +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +// EXPECT_DEATH tests don't work on Android or Windows. +#if defined(__ANDROID__) || defined(_MSC_VER) +#define NO_DEATH_TESTS +#endif // __ANDROID__ + +class IntegerListNode { + public: + explicit IntegerListNode(int value) : node(), value_(value) {} + // Older versions of Visual Studio don't generate move constructors or move + // assignment operators. + IntegerListNode(IntegerListNode&& other) { *this = std::move(other); } + IntegerListNode& operator=(IntegerListNode&& other) { + value_ = other.value_; + node = std::move(other.node); + return *this; + } + + int value() const { return value_; } + firebase::intrusive_list_node node; // NOLINT + + private: + int value_; + + // Disallow copying. + IntegerListNode(const IntegerListNode&); + IntegerListNode& operator=(const IntegerListNode&); +}; + +bool IntegerListNodeComparitor(const IntegerListNode& a, + const IntegerListNode& b) { + return a.value() < b.value(); +} + +bool operator<(const IntegerListNode& a, const IntegerListNode& b) { + return a.value() < b.value(); +} + +bool operator==(const IntegerListNode& a, const IntegerListNode& b) { + return a.value() == b.value(); +} + +class intrusive_list_test : public testing::Test { + protected: + intrusive_list_test() + : list_(&IntegerListNode::node), + one_(1), + two_(2), + three_(3), + four_(4), + five_(5), + six_(6), + seven_(7), + eight_(8), + nine_(9), + ten_(10), + twenty_(20), + thirty_(30), + fourty_(40), + fifty_(50) {} + + firebase::intrusive_list list_; + IntegerListNode one_; + IntegerListNode two_; + IntegerListNode three_; + IntegerListNode four_; + IntegerListNode five_; + IntegerListNode six_; + IntegerListNode seven_; + IntegerListNode eight_; + IntegerListNode nine_; + IntegerListNode ten_; + IntegerListNode twenty_; + IntegerListNode thirty_; + IntegerListNode fourty_; + IntegerListNode fifty_; +}; + +TEST_F(intrusive_list_test, push_back) { + EXPECT_TRUE(!one_.node.in_list()); + EXPECT_TRUE(!two_.node.in_list()); + EXPECT_TRUE(!three_.node.in_list()); + EXPECT_TRUE(!four_.node.in_list()); + EXPECT_TRUE(!five_.node.in_list()); + + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_EQ(1, list_.front().value()); + EXPECT_EQ(5, list_.back().value()); +} + +#ifndef NO_DEATH_TESTS +TEST_F(intrusive_list_test, push_back_failure) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + EXPECT_DEATH(list_.push_back(five_), "."); +} +#endif // NO_DEATH_TESTS + +TEST_F(intrusive_list_test, pop_back) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + EXPECT_EQ(5, list_.back().value()); + list_.pop_back(); + EXPECT_EQ(4, list_.back().value()); + list_.pop_back(); + EXPECT_EQ(3, list_.back().value()); + list_.pop_back(); + list_.push_back(four_); + EXPECT_EQ(4, list_.back().value()); +} + +TEST_F(intrusive_list_test, push_front) { + list_.push_front(one_); + list_.push_front(two_); + list_.push_front(three_); + list_.push_front(four_); + list_.push_front(five_); + + auto iter = list_.begin(); + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_EQ(5, list_.front().value()); + EXPECT_EQ(1, list_.back().value()); +} + +#ifndef NO_DEATH_TESTS +TEST_F(intrusive_list_test, push_front_failure) { + list_.push_front(five_); + list_.push_front(four_); + list_.push_front(three_); + list_.push_front(two_); + list_.push_front(one_); + EXPECT_DEATH(list_.push_front(one_), "."); +} +#endif // NO_DEATH_TESTS + +TEST_F(intrusive_list_test, destructor) { + list_.push_back(one_); + list_.push_back(two_); + { + // These should remove themselves when they go out of scope. + IntegerListNode one_hundred(100); + IntegerListNode two_hundred(200); + list_.push_back(one_hundred); + list_.push_back(two_hundred); + } + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_EQ(1, list_.front().value()); + EXPECT_EQ(5, list_.back().value()); +} + +TEST_F(intrusive_list_test, move_node) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + // Generally, when moving something it would be done implicitly when the + // object holding it moves. This is just to demonstrate that it moves the + // pointers around correctly when it does move. + // + // two_.node has four_.node's location in the list moved into it. four_.node + // is left in a valid but unspecified state. + two_.node = std::move(four_.node); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, rbegin_rend) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.rbegin(); + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(list_.rend(), iter); +} + +TEST_F(intrusive_list_test, crbegin_crend) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.crbegin(); + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(list_.crend(), iter); +} + +TEST_F(intrusive_list_test, clear) { + EXPECT_TRUE(list_.empty()); + + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + EXPECT_FALSE(list_.empty()); + + list_.clear(); + EXPECT_TRUE(list_.empty()); +} + +TEST_F(intrusive_list_test, insert) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + ++iter; + list_.insert(iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_before) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + ++iter; + firebase::intrusive_list:: + insert_before<&IntegerListNode::node>(*iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_after) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + firebase::intrusive_list:: + insert_after<&IntegerListNode::node>(*iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_begin) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + list_.insert(iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_end) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + ++iter; + ++iter; + ++iter; + ++iter; + list_.insert(iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_iter) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + std::vector list_nodes; + list_nodes.push_back(IntegerListNode(100)); + list_nodes.push_back(IntegerListNode(200)); + list_nodes.push_back(IntegerListNode(300)); + + auto iter = list_.begin(); + ++iter; + ++iter; + list_.insert(iter, list_nodes.begin(), list_nodes.end()); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(100, iter->value()); + ++iter; + EXPECT_EQ(200, iter->value()); + ++iter; + EXPECT_EQ(300, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, size) { + EXPECT_EQ(0u, list_.size()); + EXPECT_TRUE(list_.empty()); + list_.push_back(one_); + EXPECT_EQ(1u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_back(two_); + EXPECT_EQ(2u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_front(three_); + EXPECT_EQ(3u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_back(four_); + EXPECT_EQ(4u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_front(five_); + EXPECT_EQ(5u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_front(); + EXPECT_EQ(4u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_back(); + EXPECT_EQ(3u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_front(); + EXPECT_EQ(2u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_back(); + EXPECT_EQ(1u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_front(); + EXPECT_EQ(0u, list_.size()); + EXPECT_TRUE(list_.empty()); +} + +TEST_F(intrusive_list_test, unique) { + IntegerListNode another_one(1); + IntegerListNode another_three(3); + IntegerListNode another_five(5); + IntegerListNode another_five_again(5); + + list_.push_back(one_); + list_.push_back(another_one); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(another_three); + list_.push_back(four_); + list_.push_back(five_); + list_.push_back(another_five); + list_.push_back(another_five_again); + + list_.unique(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + EXPECT_TRUE(!another_one.node.in_list()); + EXPECT_TRUE(!another_three.node.in_list()); + EXPECT_TRUE(!another_five.node.in_list()); + EXPECT_TRUE(!another_five_again.node.in_list()); +} + +TEST_F(intrusive_list_test, unique_predicate) { + IntegerListNode another_one(1); + IntegerListNode another_three(3); + IntegerListNode another_five(5); + IntegerListNode another_five_again(5); + + list_.push_back(one_); + list_.push_back(another_one); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(another_three); + list_.push_back(four_); + list_.push_back(five_); + list_.push_back(another_five); + list_.push_back(another_five_again); + + list_.unique(std::equal_to()); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + EXPECT_TRUE(!another_one.node.in_list()); + EXPECT_TRUE(!another_three.node.in_list()); + EXPECT_TRUE(!another_five.node.in_list()); + EXPECT_TRUE(!another_five_again.node.in_list()); +} + +TEST_F(intrusive_list_test, sort_in_order) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, sort_reverse_order) { + list_.push_back(five_); + list_.push_back(four_); + list_.push_back(three_); + list_.push_back(two_); + list_.push_back(one_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, sort_random_order) { + list_.push_back(two_); + list_.push_back(four_); + list_.push_back(five_); + list_.push_back(one_); + list_.push_back(three_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, sort_short_list) { + list_.push_back(two_); + list_.push_back(one_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, splice_empty) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + + list_.splice(list_.begin(), other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, splice_other_at_beginning) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(ten_); + other_list.push_back(twenty_); + other_list.push_back(thirty_); + other_list.push_back(fourty_); + other_list.push_back(fifty_); + + list_.splice(list_.begin(), other_list); + + auto iter = list_.begin(); + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, splice_other_at_end) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(ten_); + other_list.push_back(twenty_); + other_list.push_back(thirty_); + other_list.push_back(fourty_); + other_list.push_back(fifty_); + + list_.splice(list_.end(), other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, splice_other_at_middle) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(ten_); + other_list.push_back(twenty_); + other_list.push_back(thirty_); + other_list.push_back(fourty_); + other_list.push_back(fifty_); + + auto iter = list_.begin(); + ++iter; + ++iter; + ++iter; + list_.splice(iter, other_list); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_alternating) { + list_.push_back(one_); + list_.push_back(three_); + list_.push_back(five_); + list_.push_back(seven_); + list_.push_back(nine_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(two_); + other_list.push_back(four_); + other_list.push_back(six_); + other_list.push_back(eight_); + other_list.push_back(ten_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_alternating2) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(five_); + list_.push_back(six_); + list_.push_back(nine_); + list_.push_back(ten_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(three_); + other_list.push_back(four_); + other_list.push_back(seven_); + other_list.push_back(eight_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_this_other) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(six_); + other_list.push_back(seven_); + other_list.push_back(eight_); + other_list.push_back(nine_); + other_list.push_back(ten_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_other_this) { + list_.push_back(six_); + list_.push_back(seven_); + list_.push_back(eight_); + list_.push_back(nine_); + list_.push_back(ten_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(one_); + other_list.push_back(two_); + other_list.push_back(three_); + other_list.push_back(four_); + other_list.push_back(five_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +#if defined(FIREBASE_USE_MOVE_OPERATORS) +TEST_F(intrusive_list_test, move_constructor) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other(std::move(list_)); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + + auto iter = other.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(other.end(), iter); +} + +TEST_F(intrusive_list_test, move_assignment) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other(&IntegerListNode::node); + other = std::move(list_); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + + auto iter = other.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(other.end(), iter); +} +#endif + +TEST_F(intrusive_list_test, swap) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other(&IntegerListNode::node); + other.push_back(ten_); + other.push_back(twenty_); + other.push_back(thirty_); + other.push_back(fourty_); + other.push_back(fifty_); + + list_.swap(other); + + auto iter = list_.begin(); + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(other.end(), iter); +} + +TEST_F(intrusive_list_test, swap_self) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + list_.swap(list_); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, erase_iterator) { + IntegerListNode *e[10]; + + // Create a list with 10 items. + for (int i = 0; i < 10; ++i) { + e[i] = new IntegerListNode(i); + list_.push_back(*e[i]); + } + + // Test that erase(iterator) works. + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(list_.size(), 10 - i); + + using iterator = firebase::intrusive_list::iterator; + iterator it = std::find(list_.begin(), list_.end(), IntegerListNode(i)); + iterator next_it = list_.erase(it); + if (i == 9) { + EXPECT_EQ(next_it, list_.end()); + } else { + EXPECT_NE(next_it->value(), i); + } + + EXPECT_EQ(list_.size(), 10 - i - 1); + delete e[i]; + } +} + +TEST_F(intrusive_list_test, erase_range) { + IntegerListNode *e[10]; + + // Create a list with 10 items. + for (int i = 0; i < 10; ++i) { + e[i] = new IntegerListNode(i); + list_.push_back(*e[i]); + } + + using iterator = firebase::intrusive_list::iterator; + + // Test that erase(iterator, iterator) with a null range has no effect. + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(list_.size(), 10); + iterator range_begin = std::find(list_.begin(), list_.end(), + IntegerListNode(i)); + iterator range_end = range_begin; + iterator result = list_.erase(range_begin, range_end); + EXPECT_EQ(list_.size(), 10); + EXPECT_EQ(result, range_end); + EXPECT_NE(result, list_.end()); + EXPECT_EQ(result->value(), i); + } + + // Test that erase(iterator, iterator) with a non-empty range works. + for (int i = 0; i < 10; i += 2) { + EXPECT_EQ(list_.size(), 10 - i); + iterator range_begin = std::find(list_.begin(), list_.end(), + IntegerListNode(i)); + iterator range_end = std::find(list_.begin(), list_.end(), + IntegerListNode(i + 2)); + iterator result = list_.erase(range_begin, range_end); + EXPECT_EQ(result, range_end); + if (i + 2 == 10) { + EXPECT_EQ(result, list_.end()); + } else { + EXPECT_EQ(result->value(), i + 2); + } + EXPECT_EQ(list_.size(), 10 - i - 2); + delete e[i]; + delete e[i + 1]; + } + EXPECT_EQ(list_.size(), 0); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/app/tests/jobject_reference_test.cc b/app/tests/jobject_reference_test.cc new file mode 100644 index 0000000000..06fe955085 --- /dev/null +++ b/app/tests/jobject_reference_test.cc @@ -0,0 +1,162 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/jobject_reference.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +// For firebase::util::JStringToString. +#include "app/src/util_android.h" +#include "testing/run_all_tests.h" + +using firebase::internal::JObjectReference; +using firebase::util::JStringToString; +using testing::Eq; +using testing::IsNull; +using testing::NotNull; + +JOBJECT_REFERENCE(JObjectReferenceAlias); + +// Tests for JObjectReference. +class JObjectReferenceTest : public ::testing::Test { + protected: + void SetUp() override { + env_ = firebase::testing::cppsdk::GetTestJniEnv(); + ASSERT_TRUE(env_ != nullptr); + } + + JNIEnv *env_; + + static const char *const kTestString; +}; + +const char *const JObjectReferenceTest::kTestString = "Testing testing 1 2 3"; + +TEST_F(JObjectReferenceTest, ConstructEmpty) { + JObjectReference ref(env_); + JObjectReferenceAlias alias(env_); + EXPECT_THAT(ref.GetJNIEnv(), Eq(env_)); + EXPECT_THAT(ref.java_vm(), NotNull()); + EXPECT_THAT(ref.object(), IsNull()); + EXPECT_THAT(*ref, IsNull()); + EXPECT_THAT(alias.GetJNIEnv(), Eq(env_)); + EXPECT_THAT(alias.java_vm(), NotNull()); + EXPECT_THAT(alias.object(), IsNull()); + EXPECT_THAT(*alias, IsNull()); +} + +TEST_F(JObjectReferenceTest, ConstructDestruct) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref(env_, java_string); + JObjectReferenceAlias alias(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_THAT(JStringToString(env_, ref.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, *ref), Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, *alias), Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, CopyConstruct) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref1(env_, java_string); + env_->DeleteLocalRef(java_string); + JObjectReference ref2(ref1); + JObjectReferenceAlias alias1(ref1); + JObjectReferenceAlias alias2(alias1); + EXPECT_THAT(JStringToString(env_, ref1.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, ref2.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias1.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias2.object()), + Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, Move) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref1(env_, java_string); + env_->DeleteLocalRef(java_string); + JObjectReference ref2 = std::move(ref1); + EXPECT_THAT(JStringToString(env_, ref2.object()), + Eq(std::string(kTestString))); + JObjectReferenceAlias alias1(std::move(ref2)); + EXPECT_THAT(JStringToString(env_, alias1.object()), + Eq(std::string(kTestString))); + JObjectReferenceAlias alias2(env_); + alias2 = std::move(alias1); + EXPECT_THAT(JStringToString(env_, alias2.object()), + Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, Copy) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref1(env_, java_string); + env_->DeleteLocalRef(java_string); + JObjectReference ref2(env_); + ref2 = ref1; + JObjectReferenceAlias alias(env_); + alias = ref2; + EXPECT_THAT(JStringToString(env_, ref1.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, ref2.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias.object()), + Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, Set) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_THAT(JStringToString(env_, ref.object()), + Eq(std::string(kTestString))); + ref.Set(nullptr); + EXPECT_THAT(ref.object(), IsNull()); +} + +TEST_F(JObjectReferenceTest, GetLocalRef) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref(env_, java_string); + jobject local = ref.GetLocalRef(); + EXPECT_THAT(JStringToString(env_, local), Eq(std::string(kTestString))); + env_->DeleteLocalRef(local); + + JObjectReferenceAlias alias(env_, java_string); + local = alias.GetLocalRef(); + EXPECT_THAT(JStringToString(env_, local), Eq(std::string(kTestString))); + env_->DeleteLocalRef(local); +} + +TEST_F(JObjectReferenceTest, FromGlobalReference) { + jobject java_string = env_->NewStringUTF(kTestString); + jobject java_string_alias = env_->NewLocalRef(java_string); + JObjectReference ref = + JObjectReference::FromLocalReference(env_, java_string); + JObjectReferenceAlias alias( + JObjectReferenceAlias::FromLocalReference(env_, java_string_alias)); + EXPECT_NE(nullptr, ref.object()); + EXPECT_NE(nullptr, alias.object()); + + JObjectReference nullref = + JObjectReference::FromLocalReference(env_, nullptr); + JObjectReferenceAlias alias_null( + JObjectReferenceAlias::FromLocalReference(env_, nullptr)); + EXPECT_EQ(nullptr, nullref.object()); + EXPECT_EQ(nullptr, alias_null.object()); +} diff --git a/app/tests/locale_test.cc b/app/tests/locale_test.cc new file mode 100644 index 0000000000..478786894c --- /dev/null +++ b/app/tests/locale_test.cc @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/locale.h" + +#include + +#include "app/src/include/firebase/internal/platform.h" +#include "app/src/log.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#if FIREBASE_PLATFORM_WINDOWS + +#else +#include +#endif // FIREBASE_PLATFORM_WINDOWS + +namespace firebase { +namespace internal { + +class LocaleTest : public ::testing::Test {}; + +TEST_F(LocaleTest, TestGetTimezone) { + std::string tz = GetTimezone(); + LogInfo("GetTimezone() returned '%s'", tz.c_str()); + // There is not a set format for timezones, so we must assume success if it + // was non-empty. + EXPECT_NE(tz, ""); +} + +TEST_F(LocaleTest, TestGetLocale) { + std::string loc = GetLocale(); + LogInfo("GetLocale() returned '%s'", loc.c_str()); + EXPECT_NE(loc, ""); + // Make sure this looks like a locale, e.g. has at least 5 characters and + // contains an underscore. + EXPECT_GE(loc.size(), 5); + EXPECT_NE(loc.find("_"), std::string::npos); +} + +} // namespace internal +} // namespace firebase diff --git a/app/tests/log_test.cc b/app/tests/log_test.cc new file mode 100644 index 0000000000..30b1b4783e --- /dev/null +++ b/app/tests/log_test.cc @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/log.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +// The test-cases here are by no means exhaustive. We only make sure the log +// code does not break. Whether logs are output is highly device-dependent and +// testing that is not right now the main goal here. + +TEST(LogTest, TestSetAndGetLogLevel) { + // Try to set log-level and verify we get what we set. + SetLogLevel(kLogLevelDebug); + EXPECT_EQ(kLogLevelDebug, GetLogLevel()); + + SetLogLevel(kLogLevelError); + EXPECT_EQ(kLogLevelError, GetLogLevel()); +} + +TEST(LogDeathTest, TestLogAssert) { + // Try to make assertion and verify it dies. + SetLogLevel(kLogLevelVerbose); +// Somehow the death test does not work on ios emulator. +#if !defined(__APPLE__) + EXPECT_DEATH(LogAssert("should die"), ""); +#endif // !defined(__APPLE__) +} + +TEST(LogTest, TestLogLevelBelowAssert) { + // Try other non-aborting log levels. + SetLogLevel(kLogLevelVerbose); + // TODO(zxu): Try to catch the logs using log callback in order to verify the + // log message. + LogDebug("debug message"); + LogInfo("info message"); + LogWarning("warning message"); + LogError("error message"); +} + +} // namespace firebase diff --git a/app/tests/logger_test.cc b/app/tests/logger_test.cc new file mode 100644 index 0000000000..d082afed3f --- /dev/null +++ b/app/tests/logger_test.cc @@ -0,0 +1,283 @@ +// Copyright 2019 Google LLC +// +// 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 "app/src/logger.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace internal { +namespace { + +static const size_t kBufferSize = 100; + +class FakeLogger : public LoggerBase { + public: + FakeLogger() + : logged_message_(), + logged_message_level_(static_cast(-1)), + log_level_(kLogLevelInfo) {} + + void SetLogLevel(LogLevel log_level) override { log_level_ = log_level; } + LogLevel GetLogLevel() const override { return log_level_; } + + const std::string& logged_message() const { return logged_message_; } + LogLevel logged_message_level() const { return logged_message_level_; } + + private: + void LogMessageImplV(LogLevel log_level, const char* format, + va_list args) const override { + logged_message_level_ = log_level; + char buffer[kBufferSize]; + vsnprintf(buffer, kBufferSize, format, args); + logged_message_ = buffer; + } + + mutable std::string logged_message_; + mutable LogLevel logged_message_level_; + + mutable LogLevel log_level_; +}; + +TEST(LoggerTest, GetSetLogLevel) { + Logger logger(nullptr); + EXPECT_EQ(logger.GetLogLevel(), kLogLevelInfo); + logger.SetLogLevel(kLogLevelVerbose); + EXPECT_EQ(logger.GetLogLevel(), kLogLevelVerbose); + + Logger logger2(nullptr, kLogLevelDebug); + EXPECT_EQ(logger2.GetLogLevel(), kLogLevelDebug); + logger2.SetLogLevel(kLogLevelInfo); + EXPECT_EQ(logger2.GetLogLevel(), kLogLevelInfo); +} + +TEST(LoggerTest, LogWithEachFunction) { + FakeLogger logger; + + // Ensure everything gets through. + logger.SetLogLevel(kLogLevelVerbose); + + logger.LogDebug("LogDebug %i", 1); + EXPECT_EQ(logger.logged_message_level(), kLogLevelDebug); + EXPECT_EQ(logger.logged_message(), "LogDebug 1"); + + logger.LogInfo("LogInfo %i", 2); + EXPECT_EQ(logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(logger.logged_message(), "LogInfo 2"); + + logger.LogWarning("LogWarning %i", 3); + EXPECT_EQ(logger.logged_message_level(), kLogLevelWarning); + EXPECT_EQ(logger.logged_message(), "LogWarning 3"); + + logger.LogError("LogError %i", 4); + EXPECT_EQ(logger.logged_message_level(), kLogLevelError); + EXPECT_EQ(logger.logged_message(), "LogError 4"); + + logger.LogAssert("LogAssert %i", 5); + EXPECT_EQ(logger.logged_message_level(), kLogLevelAssert); + EXPECT_EQ(logger.logged_message(), "LogAssert 5"); + + logger.LogMessage(kLogLevelInfo, "LogMessage %i", 6); + EXPECT_EQ(logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(logger.logged_message(), "LogMessage 6"); +} + +TEST(LoggerTest, FilteringPermissive) { + FakeLogger logger; + + logger.SetLogLevel(kLogLevelVerbose); + + logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(logger.logged_message(), "Verbose log"); + + logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(logger.logged_message(), "Debug log"); + + logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(logger.logged_message(), "Info log"); + + logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(logger.logged_message(), "Warning log"); + + logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(logger.logged_message(), "Error log"); + + logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, FilteringMiddling) { + FakeLogger logger; + + logger.SetLogLevel(kLogLevelWarning); + + logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(logger.logged_message(), "Warning log"); + + logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(logger.logged_message(), "Error log"); + + logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, FilteringStrict) { + FakeLogger logger; + + logger.SetLogLevel(kLogLevelAssert); + + logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, ChainedLogWithEachFunction) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelVerbose); + child_logger.SetLogLevel(kLogLevelVerbose); + + child_logger.LogDebug("LogDebug %i", 1); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelDebug); + EXPECT_EQ(parent_logger.logged_message(), "LogDebug 1"); + + child_logger.LogInfo("LogInfo %i", 2); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(parent_logger.logged_message(), "LogInfo 2"); + + child_logger.LogWarning("LogWarning %i", 3); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelWarning); + EXPECT_EQ(parent_logger.logged_message(), "LogWarning 3"); + + child_logger.LogError("LogError %i", 4); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelError); + EXPECT_EQ(parent_logger.logged_message(), "LogError 4"); + + child_logger.LogAssert("LogAssert %i", 5); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelAssert); + EXPECT_EQ(parent_logger.logged_message(), "LogAssert 5"); + + child_logger.LogMessage(kLogLevelInfo, "LogMessage %i", 6); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(parent_logger.logged_message(), "LogMessage 6"); +} + +TEST(LoggerTest, ChainedFilteringSameLevel) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelInfo); + child_logger.SetLogLevel(kLogLevelInfo); + + child_logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(parent_logger.logged_message(), "Info log"); + + child_logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(parent_logger.logged_message(), "Warning log"); + + child_logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(parent_logger.logged_message(), "Error log"); + + child_logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(parent_logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, ChainedFilteringStricterChildLogger) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelInfo); + child_logger.SetLogLevel(kLogLevelError); + + child_logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(parent_logger.logged_message(), "Error log"); + + child_logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(parent_logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, ChainedFilteringMorePermissiveChildLogger) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelError); + child_logger.SetLogLevel(kLogLevelInfo); + + child_logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(parent_logger.logged_message(), "Error log"); + + child_logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(parent_logger.logged_message(), "Assert log"); +} + +} // namespace +} // namespace internal +} // namespace firebase diff --git a/app/tests/optional_test.cc b/app/tests/optional_test.cc new file mode 100644 index 0000000000..396ea86799 --- /dev/null +++ b/app/tests/optional_test.cc @@ -0,0 +1,446 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/optional.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// Using Mocks to count the number of times a constructor, destructor, +// or (move|copy) (constructor|assignment operator) is called is not easy to do +// directly because gMock requires marking the methods to be called virtual. +// Instead, we create a special wrapper Mock object that calls these virtual +// functions instead, so that they can be counted. +class SpecialFunctionsNotifier { + public: + virtual ~SpecialFunctionsNotifier() {} + + virtual void Construct() = 0; + virtual void Copy() = 0; +#ifdef FIREBASE_USE_MOVE_OPERATORS + virtual void Move() = 0; +#endif + virtual void Destruct() = 0; +}; + +// This class exists only to call through to the underlying +// SpecialFunctionNotifier so that those calls can be counted. +class SpecialFunctionsNotifierWrapper { + public: + SpecialFunctionsNotifierWrapper() { s_notifier_->Construct(); } + SpecialFunctionsNotifierWrapper( + const SpecialFunctionsNotifierWrapper& other) { + s_notifier_->Copy(); + } + SpecialFunctionsNotifierWrapper& operator=( + const SpecialFunctionsNotifierWrapper& other) { + s_notifier_->Copy(); + return *this; + } + +#ifdef FIREBASE_USE_MOVE_OPERATORS + SpecialFunctionsNotifierWrapper(SpecialFunctionsNotifierWrapper&& other) { + s_notifier_->Move(); + } + SpecialFunctionsNotifierWrapper& operator=( + SpecialFunctionsNotifierWrapper&& other) { + s_notifier_->Move(); + return *this; + } +#endif + + ~SpecialFunctionsNotifierWrapper() { s_notifier_->Destruct(); } + + static SpecialFunctionsNotifier* s_notifier_; +}; + +SpecialFunctionsNotifier* SpecialFunctionsNotifierWrapper::s_notifier_ = + nullptr; + +class SpecialFunctionsNotifierMock : public SpecialFunctionsNotifier { + public: + MOCK_METHOD(void, Construct, (), (override)); + MOCK_METHOD(void, Copy, (), (override)); +#ifdef FIREBASE_USE_MOVE_OPERATORS + MOCK_METHOD(void, Move, (), (override)); +#endif + MOCK_METHOD(void, Destruct, (), (override)); +}; + +// A simple class with a method on it, used for testing the arrow operator of +// Optional. +class IntHolder { + public: + explicit IntHolder(int value) : value_(value) {} + int GetValue() const { return value_; } + + private: + int value_; +}; + +// Helper class used to setup mock expect calls due to the complexities of move +// enabled or not +class ExpectCallSetup { + public: + explicit ExpectCallSetup(SpecialFunctionsNotifierMock* mock_notifier) + : mock_notifier_(mock_notifier) {} + + ExpectCallSetup& Construct(size_t expectecCallCount) { + EXPECT_CALL(*mock_notifier_, Construct()).Times(expectecCallCount); + return *this; + } + + ExpectCallSetup& CopyAndMove(size_t expectecCopyCallCount, + size_t expectecMoveCallCount) { +#ifdef FIREBASE_USE_MOVE_OPERATORS + EXPECT_CALL(*mock_notifier_, Copy()).Times(expectecCopyCallCount); + EXPECT_CALL(*mock_notifier_, Move()).Times(expectecMoveCallCount); +#else + EXPECT_CALL(*mock_notifier_, Copy()). + Times(expectecCopyCallCount + expectecMoveCallCount); +#endif + return *this; + } + + ExpectCallSetup& Destruct(size_t expectecCallCount) { + EXPECT_CALL(*mock_notifier_, Destruct()).Times(expectecCallCount); + return *this; + } + + SpecialFunctionsNotifierMock* mock_notifier_; +}; + +class OptionalTest : public ::testing::Test, protected ExpectCallSetup { + protected: + OptionalTest() + : ExpectCallSetup(&mock_notifier_) + {} + + void SetUp() override { + SpecialFunctionsNotifierWrapper::s_notifier_ = &mock_notifier_; + } + + void TearDown() override { + SpecialFunctionsNotifierWrapper::s_notifier_ = nullptr; + } + + ExpectCallSetup& SetupExpectCall() { + return *this; + } + + SpecialFunctionsNotifierMock mock_notifier_; +}; + +TEST_F(OptionalTest, DefaultConstructor) { + Optional optional_int; + EXPECT_FALSE(optional_int.has_value()); + + Optional optional_struct; + EXPECT_FALSE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, CopyConstructor) { + Optional optional_int(9999); + + Optional copy_of_optional_int(optional_int); + EXPECT_TRUE(copy_of_optional_int.has_value()); + EXPECT_EQ(copy_of_optional_int.value(), 9999); + + Optional another_copy_of_optional_int = optional_int; + EXPECT_TRUE(another_copy_of_optional_int.has_value()); + EXPECT_EQ(another_copy_of_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(2, 1) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + + Optional copy_of_optional_struct( + optional_struct); + EXPECT_TRUE(copy_of_optional_struct.has_value()); + + Optional another_copy_of_optional_struct = + optional_struct; + EXPECT_TRUE(another_copy_of_optional_struct.has_value()); +} + +TEST_F(OptionalTest, CopyAssignment) { + Optional optional_int(9999); + Optional another_optional_int(42); + another_optional_int = optional_int; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + EXPECT_TRUE(another_optional_int.has_value()); + EXPECT_EQ(another_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(2) + .CopyAndMove(1, 2) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + Optional another_optional_struct( + SpecialFunctionsNotifierWrapper{}); + another_optional_struct = optional_struct; + EXPECT_TRUE(optional_struct.has_value()); + EXPECT_TRUE(another_optional_struct.has_value()); +} + +TEST_F(OptionalTest, CopyAssignmentSelf) { + Optional optional_int(9999); + optional_int = *&optional_int; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(1, 1) + .Destruct(2); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + optional_struct = *&optional_struct; + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, MoveConstructor) { + Optional optional_int(9999); + + Optional moved_optional_int(std::move(optional_int)); + EXPECT_TRUE(moved_optional_int.has_value()); + EXPECT_EQ(moved_optional_int.value(), 9999); + + Optional another_moved_optional_int = std::move(moved_optional_int); + EXPECT_TRUE(another_moved_optional_int.has_value()); + EXPECT_EQ(another_moved_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(0, 3) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + + Optional copy_of_optional_struct( + std::move(optional_struct)); + EXPECT_TRUE(copy_of_optional_struct.has_value()); + + Optional another_copy_of_optional_struct = + std::move(copy_of_optional_struct); + EXPECT_TRUE(another_copy_of_optional_struct.has_value()); +} + +TEST_F(OptionalTest, MoveAssignment) { + Optional optional_int(9999); + Optional another_optional_int(42); + another_optional_int = std::move(optional_int); + + EXPECT_TRUE(another_optional_int.has_value()); + EXPECT_EQ(another_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(2) + .CopyAndMove(0, 3) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + Optional another_optional_struct( + SpecialFunctionsNotifierWrapper{}); + another_optional_struct = std::move(optional_struct); + + EXPECT_TRUE(another_optional_struct.has_value()); +} + +TEST_F(OptionalTest, Destructor) { + SetupExpectCall() + .Construct(2) + .CopyAndMove(0, 2) + .Destruct(4); + + // Verify the destructor is called when object goes out of scope. + { + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + (void)optional_struct; + } + // Verify the destructor is called when reset is called. + { + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + optional_struct.reset(); + } +} + +TEST_F(OptionalTest, ValueConstructor) { + Optional optional_int(1337); + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 1337); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(1, 0) + .Destruct(2); + + SpecialFunctionsNotifierWrapper value{}; + Optional optional_struct(value); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueMoveConstructor) { + SetupExpectCall() + .Construct(1) + .CopyAndMove(0, 1) + .Destruct(2); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueCopyAssignmentToUnpopulatedOptional) { + Optional optional_int; + optional_int = 9999; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(1, 0) + .Destruct(2); + + Optional optional_struct; + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = my_struct; + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueCopyAssignmentToPopulatedOptional) { + Optional optional_int(27); + optional_int = 9999; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + + SetupExpectCall() + .Construct(2) + .CopyAndMove(1, 1) + .Destruct(3); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = my_struct; + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueMoveAssignmentToUnpopulatedOptional) { + SetupExpectCall() + .Construct(1) + .CopyAndMove(0, 1) + .Destruct(2); + + Optional optional_struct; + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = std::move(my_struct); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueMoveAssignmentToPopulatedOptional) { + SetupExpectCall() + .Construct(2) + .CopyAndMove(0, 2) + .Destruct(3); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = std::move(my_struct); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ArrowOperator) { + Optional optional_int_holder(IntHolder(12345)); + EXPECT_EQ(optional_int_holder->GetValue(), 12345); +} + +TEST_F(OptionalTest, HasValue) { + Optional optional_int; + EXPECT_FALSE(optional_int.has_value()); + + optional_int = 12345; + EXPECT_TRUE(optional_int.has_value()); + + optional_int.reset(); + EXPECT_FALSE(optional_int.has_value()); +} + +TEST_F(OptionalTest, ValueDeathTest) { + Optional empty; + EXPECT_DEATH(empty.value(), ""); +} + +TEST_F(OptionalTest, ValueOr) { + Optional optional_int; + EXPECT_EQ(optional_int.value_or(67890), 67890); + + optional_int = 12345; + EXPECT_EQ(optional_int.value_or(67890), 12345); +} + +TEST_F(OptionalTest, EqualityOperator) { + Optional lhs(123456); + Optional rhs(123456); + Optional wrong(654321); + Optional empty; + Optional another_empty; + + EXPECT_TRUE(lhs == rhs); + EXPECT_FALSE(lhs != rhs); + EXPECT_FALSE(lhs == wrong); + EXPECT_TRUE(lhs != wrong); + + EXPECT_FALSE(empty == rhs); + EXPECT_TRUE(empty != rhs); + EXPECT_TRUE(empty == another_empty); + EXPECT_FALSE(empty != another_empty); +} + +TEST_F(OptionalTest, OptionalFromPointer) { + int value = 100; + int* value_ptr = &value; + int* value_nullptr = nullptr; + Optional optional_with_value = OptionalFromPointer(value_ptr); + Optional optional_without_value = OptionalFromPointer(value_nullptr); + + EXPECT_TRUE(optional_with_value.has_value()); + EXPECT_EQ(optional_with_value.value(), 100); + EXPECT_FALSE(optional_without_value.has_value()); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/app/tests/path_test.cc b/app/tests/path_test.cc new file mode 100644 index 0000000000..bdce82eda4 --- /dev/null +++ b/app/tests/path_test.cc @@ -0,0 +1,452 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/path.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +using ::firebase::Optional; +using ::firebase::Path; +using ::testing::Eq; +using ::testing::StrEq; + +TEST(PathTests, DefaultConstructor) { + Path path; + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_TRUE(path.empty()); +} + +TEST(PathTests, StringConstructor) { + Path path; + + // Empty string + path = Path(""); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); + + // Root folder + path = Path("/"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); + + // Root Folder with plenty slashes + path = Path("//////"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); + + // Correctly formatted string. + path = Path("test/foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Leading slash. + path = Path("/test/foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Trailing slash. + path = Path("test/foo/bar/"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Leading and trailing slash. + path = Path("/test/foo/bar/"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Internal slashes. + path = Path("/test/////foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Slashes everywhere! + path = Path("///test/////foo//bar///"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Backslashes. + path = Path("///test\\foo\\bar///"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test\\foo\\bar")); + EXPECT_THAT(path.str(), StrEq("test\\foo\\bar")); + EXPECT_THAT(path.c_str(), StrEq("test\\foo\\bar")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, VectorConstructor) { + Path path; + std::vector directories; + + // Directories with no slashes. + directories = {"test", "foo", "bar"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with extraneous slashes. + directories = {"/test/", "/foo", "bar/"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string. + directories = {"test/foo", "bar"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string with extraneous slashes. + directories = {"/test/", "/foo/bar/"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, VectorIteratorConstructor) { + Path path; + std::vector directories; + + // Directories with no slashes. + directories = {"test", "foo", "bar"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with extraneous slashes. + directories = {"/test/", "/foo", "bar/"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string. + directories = {"test/foo", "bar"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string with extraneous slashes. + directories = {"/test/", "/foo/bar/"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with no slashes, starting from the second element. + directories = {"test", "foo", "bar"}; + path = Path(++directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with no slashes, ending before the last element. + directories = {"test", "foo", "bar"}; + path = Path(directories.begin(), --directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + // Directories with no slashes, starting from the second element and ending + // before the last element. + directories = {"test", "foo", "bar"}; + path = Path(++directories.begin(), --directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("foo")); + EXPECT_THAT(path.c_str(), StrEq("foo")); + EXPECT_FALSE(path.empty()); + + // Starting and ending at the sample place. + directories = {"test", "foo", "bar"}; + path = Path(directories.begin(), directories.begin()); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); +} + +TEST(PathTests, GetParent) { + Path path; + + path = Path("/test/foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + path = path.GetParent(); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + path = path.GetParent(); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test")); + EXPECT_THAT(path.str(), StrEq("test")); + EXPECT_THAT(path.c_str(), StrEq("test")); + EXPECT_FALSE(path.empty()); + + path = path.GetParent(); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); +} + +TEST(PathTests, GetChildWithString) { + Path path; + + path = path.GetChild("test"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test")); + EXPECT_THAT(path.str(), StrEq("test")); + EXPECT_THAT(path.c_str(), StrEq("test")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild("foo"); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild("bar/baz"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.GetBaseName(), StrEq("baz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild("///quux///quaaz///"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar/baz/quux")); + EXPECT_THAT(path.GetBaseName(), StrEq("quaaz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, GetChildWithPath) { + Path path; + + path = path.GetChild(Path("test")); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test")); + EXPECT_THAT(path.str(), StrEq("test")); + EXPECT_THAT(path.c_str(), StrEq("test")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild(Path("foo")); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild(Path("bar/baz")); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.GetBaseName(), StrEq("baz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild(Path("///quux///quaaz///")); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar/baz/quux")); + EXPECT_THAT(path.GetBaseName(), StrEq("quaaz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, IsParent) { + Path path("foo/bar/baz"); + + EXPECT_TRUE(Path().IsParent(Path())); + + EXPECT_TRUE(Path().IsParent(path)); + EXPECT_TRUE(Path("foo").IsParent(path)); + EXPECT_TRUE(Path("foo/").IsParent(path)); + EXPECT_TRUE(Path("foo/bar").IsParent(path)); + EXPECT_TRUE(Path("foo/bar/").IsParent(path)); + EXPECT_TRUE(Path("foo/bar/baz").IsParent(path)); + EXPECT_TRUE(Path("foo/bar/baz/").IsParent(path)); + EXPECT_TRUE(path.IsParent(Path("foo/bar/baz"))); + EXPECT_TRUE(path.IsParent(Path("foo/bar/baz/"))); + EXPECT_FALSE(path.IsParent(Path("foo"))); + EXPECT_FALSE(path.IsParent(Path("foo/"))); + EXPECT_FALSE(path.IsParent(Path("foo/bar"))); + EXPECT_FALSE(path.IsParent(Path("foo/bar/"))); + + EXPECT_FALSE(Path("completely/wrong").IsParent(path)); + EXPECT_FALSE(Path("f").IsParent(path)); + EXPECT_FALSE(Path("fo").IsParent(path)); + EXPECT_FALSE(Path("foo/b").IsParent(path)); + EXPECT_FALSE(Path("foo/ba").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/b").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/ba").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/baz/q").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/baz/quux").IsParent(path)); +} + +TEST(PathTests, GetDirectories) { + std::vector golden = {"foo", "bar", "baz"}; + Path path; + + path = Path("foo/bar/baz"); + EXPECT_THAT(path.GetDirectories(), Eq(golden)); + + path = Path("//foo/bar///baz///"); + EXPECT_THAT(path.GetDirectories(), Eq(golden)); +} + +TEST(PathTests, FrontDirectory) { + EXPECT_EQ(Path().FrontDirectory(), Path()); + EXPECT_EQ(Path("single_level").FrontDirectory(), Path("single_level")); + EXPECT_EQ(Path("multi/level/directory/structure").FrontDirectory(), + Path("multi")); +} + +TEST(PathTests, PopFrontDirectory) { + EXPECT_EQ(Path().PopFrontDirectory(), Path()); + EXPECT_EQ(Path("single_level").PopFrontDirectory(), Path()); + EXPECT_EQ(Path("multi/level/directory/structure").PopFrontDirectory(), + Path("level/directory/structure")); +} + +TEST(PathTests, GetRelative) { + Path result; + + EXPECT_TRUE( + Path::GetRelative(Path(""), Path("starting/from/empty/path"), &result)); + EXPECT_EQ(result, Path("starting/from/empty/path")); + + EXPECT_TRUE(Path::GetRelative(Path("a/b/c/d/e"), + Path("a/b/c/d/e/f/g/h/i/j/k"), &result)); + EXPECT_THAT(result.str(), StrEq("f/g/h/i/j/k")); + + EXPECT_TRUE(Path::GetRelative( + Path("first_star/on_left"), + Path("first_star/on_left/straight_on/till_morning"), &result)); + EXPECT_THAT(result.str(), StrEq("straight_on/till_morning")); + + result = Path("result/left/untouched"); + + EXPECT_FALSE(Path::GetRelative(Path("some/overlap/but/failure"), + Path("some/overlap/and/unsuccessful"), + &result)); + EXPECT_THAT(result.str(), StrEq("result/left/untouched")); + + EXPECT_FALSE(Path::GetRelative(Path("no/overlap/at/all"), + Path("apple/banana/carrot"), &result)); + EXPECT_THAT(result.str(), StrEq("result/left/untouched")); + + EXPECT_FALSE(Path::GetRelative(Path("the/longer/path/comes/first/now"), + Path("the/longer/path"), &result)); + EXPECT_THAT(result.str(), StrEq("result/left/untouched")); +} + +TEST(PathTests, GetRelativeOptional) { + Optional result; + + result = Path::GetRelative(Path(""), Path("starting/from/empty/path")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, Path("starting/from/empty/path")); + + result = Path::GetRelative(Path("a/b/c/d/e"), Path("a/b/c/d/e/f/g/h/i/j/k")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, Path("f/g/h/i/j/k")); + + result = + Path::GetRelative(Path("first_star/on_left"), + Path("first_star/on_left/straight_on/till_morning")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, Path("straight_on/till_morning")); + + result = Path::GetRelative(Path("some/overlap/but/failure"), + Path("some/overlap/and/unsuccessful")); + EXPECT_FALSE(result.has_value()); + + result = + Path::GetRelative(Path("no/overlap/at/all"), Path("apple/banana/carrot")); + EXPECT_FALSE(result.has_value()); + + result = Path::GetRelative(Path("the/longer/path/comes/first/now"), + Path("the/longer/path")); + EXPECT_FALSE(result.has_value()); +} + +} // namespace diff --git a/app/tests/reference_count_test.cc b/app/tests/reference_count_test.cc new file mode 100644 index 0000000000..623f1a5ef3 --- /dev/null +++ b/app/tests/reference_count_test.cc @@ -0,0 +1,275 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/reference_count.h" + +#include "app/src/mutex.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::IsNull; + +using ::firebase::internal::ReferenceCount; +using ::firebase::internal::ReferenceCountedInitializer; +using ::firebase::internal::ReferenceCountLock; + +class ReferenceCountTest : public ::testing::Test { + protected: + ReferenceCount count_; +}; + +TEST_F(ReferenceCountTest, Construct) { + EXPECT_THAT(count_.references(), Eq(0)); +} + +TEST_F(ReferenceCountTest, AddReference) { + EXPECT_THAT(count_.AddReference(), Eq(0)); + EXPECT_THAT(count_.references(), Eq(1)); + EXPECT_THAT(count_.AddReference(), Eq(1)); + EXPECT_THAT(count_.references(), Eq(2)); +} + +TEST_F(ReferenceCountTest, RemoveReference) { + count_.AddReference(); + count_.AddReference(); + EXPECT_THAT(count_.RemoveReference(), Eq(2)); + EXPECT_THAT(count_.references(), Eq(1)); + EXPECT_THAT(count_.RemoveReference(), Eq(1)); + EXPECT_THAT(count_.references(), Eq(0)); + EXPECT_THAT(count_.RemoveReference(), Eq(0)); + EXPECT_THAT(count_.references(), Eq(0)); +} + +TEST_F(ReferenceCountTest, RemoveAllReferences) { + count_.AddReference(); + count_.AddReference(); + EXPECT_THAT(count_.RemoveAllReferences(), Eq(2)); + EXPECT_THAT(count_.references(), Eq(0)); +} + +class ReferenceCountLockTest : public ::testing::Test { + protected: + void SetUp() override { count_.AddReference(); } + + protected: + ReferenceCount count_; +}; + +TEST_F(ReferenceCountLockTest, Construct) { + { + ReferenceCountLock lock(&count_); + EXPECT_THAT(lock.references(), Eq(1)); + EXPECT_THAT(count_.references(), Eq(2)); + } + EXPECT_THAT(count_.references(), Eq(1)); +} + +TEST_F(ReferenceCountLockTest, AddReference) { + ReferenceCountLock lock(&count_); + EXPECT_THAT(lock.references(), Eq(1)); + EXPECT_THAT(lock.AddReference(), Eq(1)); + EXPECT_THAT(lock.references(), Eq(2)); +} + +TEST_F(ReferenceCountLockTest, RemoveReference) { + ReferenceCountLock lock(&count_); + lock.AddReference(); + lock.AddReference(); + EXPECT_THAT(lock.RemoveReference(), Eq(3)); + EXPECT_THAT(lock.references(), Eq(2)); + EXPECT_THAT(lock.RemoveReference(), Eq(2)); + EXPECT_THAT(lock.references(), Eq(1)); + EXPECT_THAT(lock.RemoveReference(), Eq(1)); + EXPECT_THAT(lock.references(), Eq(0)); + EXPECT_THAT(lock.RemoveReference(), Eq(0)); + EXPECT_THAT(lock.references(), Eq(0)); +} + +TEST_F(ReferenceCountLockTest, RemoveAllReferences) { + ReferenceCountLock lock(&count_); + lock.AddReference(); + EXPECT_THAT(lock.references(), Eq(2)); + EXPECT_THAT(lock.RemoveAllReferences(), Eq(2)); + EXPECT_THAT(lock.references(), Eq(0)); + EXPECT_THAT(count_.references(), Eq(0)); +} + +class ReferenceCountedInitializerTest : public ::testing::Test { + protected: + // Object to initialize in Initialize(). + struct Context { + bool initialize_success; + int initialized_count; + }; + + protected: + // Initialize the context object. + static bool Initialize(Context* context) { + if (!context->initialize_success) return false; + context->initialized_count++; + return true; + } + + static void Terminate(Context* context) { context->initialized_count--; } +}; + +TEST_F(ReferenceCountedInitializerTest, ConstructEmpty) { + ReferenceCountedInitializer initializer; + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(initializer.initialize(), IsNull()); + EXPECT_THAT(initializer.terminate(), IsNull()); + EXPECT_THAT(initializer.context(), IsNull()); + // Use the mutex accessor to instantiate the template. + firebase::MutexLock lock(initializer.mutex()); +} + +TEST_F(ReferenceCountedInitializerTest, ConstructWithTerminate) { + Context context; + ReferenceCountedInitializer initializer(Terminate, &context); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(initializer.initialize(), IsNull()); + EXPECT_THAT(initializer.terminate(), Eq(Terminate)); + EXPECT_THAT(initializer.context(), Eq(&context)); +} + +TEST_F(ReferenceCountedInitializerTest, ConstructWithInitializeAndTerminate) { + Context context; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(initializer.initialize(), Eq(Initialize)); + EXPECT_THAT(initializer.terminate(), Eq(Terminate)); + EXPECT_THAT(initializer.context(), Eq(&context)); +} + +TEST_F(ReferenceCountedInitializerTest, SetContext) { + Context context; + ReferenceCountedInitializer initializer(Initialize, Terminate, + nullptr); + EXPECT_THAT(initializer.context(), IsNull()); + initializer.set_context(&context); + EXPECT_THAT(initializer.context(), Eq(&context)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceNoInit) { + ReferenceCountedInitializer initializer(nullptr, nullptr, nullptr); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceInlineInit) { + Context context = {true, 0}; + ReferenceCountedInitializer initializer; + EXPECT_THAT(initializer.AddReference( + [](Context* state) { + state->initialized_count = 12345678; + return true; + }, + &context), + Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(12345678)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceSuccessfulInit) { + Context context = {true, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(1)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); + EXPECT_THAT(context.initialized_count, Eq(1)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceFailedInit) { + Context context = {false, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(-1)); + EXPECT_THAT(initializer.references(), Eq(0)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveReferenceNoInit) { + Context context = {true, 3}; + ReferenceCountedInitializer initializer(nullptr, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(3)); + EXPECT_THAT(initializer.RemoveReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(2)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveAllReferences) { + Context context = {true, 3}; + ReferenceCountedInitializer initializer(nullptr, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(3)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); + EXPECT_THAT(initializer.RemoveAllReferences(), Eq(2)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(2)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveAllReferencesWithoutTerminate) { + Context context = {true, 3}; + ReferenceCountedInitializer initializer(nullptr, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(3)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); + EXPECT_THAT(initializer.RemoveAllReferencesWithoutTerminate(), Eq(2)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(3)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveReferenceSuccessfulInit) { + Context context = {true, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(1)); + EXPECT_THAT(initializer.RemoveReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); + EXPECT_THAT(initializer.RemoveReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveReferenceFailedInit) { + Context context = {false, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(-1)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); + EXPECT_THAT(initializer.RemoveReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); +} diff --git a/app/tests/scheduler_test.cc b/app/tests/scheduler_test.cc new file mode 100644 index 0000000000..3a9baaf123 --- /dev/null +++ b/app/tests/scheduler_test.cc @@ -0,0 +1,369 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/scheduler.h" +#include "app/memory/atomic.h" +#include "app/src/semaphore.h" +#include "app/src/time.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace scheduler { + +using ::testing::Eq; + +class SchedulerTest : public ::testing::Test { + protected: + SchedulerTest() {} + + void SetUp() override { + atomic_count_.store(0); + while (callback_sem1_.TryWait()) {} + while (callback_sem2_.TryWait()) {} + ordered_value_.clear(); + repeat_period_ms_ = 0; + repeat_countdown_ = 0; + } + + static void SemaphorePost1() { + callback_sem1_.Post(); + } + + static void AddCount() { + atomic_count_.fetch_add(1); + callback_sem1_.Post(); + } + + static void AddValueInOrder(int v) { + ordered_value_.push_back(v); + callback_sem1_.Post(); + } + + static void RecursiveCallback(Scheduler* scheduler) { + callback_sem1_.Post(); + --repeat_countdown_; + + if (repeat_countdown_ > 0) { + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, RecursiveCallback), + repeat_period_ms_); + } + } + + static compat::Atomic atomic_count_; + static Semaphore callback_sem1_; + static Semaphore callback_sem2_; + static std::vector ordered_value_; + static int repeat_period_ms_; + static int repeat_countdown_; + + Scheduler scheduler_; +}; + +compat::Atomic SchedulerTest::atomic_count_(0); +Semaphore SchedulerTest::callback_sem1_(0); // NOLINT +Semaphore SchedulerTest::callback_sem2_(0); // NOLINT +std::vector SchedulerTest::ordered_value_; // NOLINT +int SchedulerTest::repeat_period_ms_ = 0; +int SchedulerTest::repeat_countdown_ = 0; + +// 10000 seems to be a good number to surface racing condition. +const int kThreadTestIteration = 10000; + +TEST_F(SchedulerTest, Basic) { + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1)); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1), 1); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); +} + +#ifdef FIREBASE_USE_STD_FUNCTION +TEST_F(SchedulerTest, BasicStdFunction) { + std::function func = [this](){ + callback_sem1_.Post(); + }; + + scheduler_.Schedule(func); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + + scheduler_.Schedule(func, 1); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); +} +#endif + +TEST_F(SchedulerTest, TriggerOrderNoDelay) { + std::vector expected; + for (int i = 0; i < kThreadTestIteration; ++i) + { + scheduler_.Schedule( + new callback::CallbackValue1( + i, AddValueInOrder)); + expected.push_back(i); + } + + for (int i = 0; i < kThreadTestIteration; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } + EXPECT_THAT(ordered_value_, Eq(expected)); +} + +TEST_F(SchedulerTest, TriggerOrderSameDelay) { + std::vector expected; + for (int i = 0; i < kThreadTestIteration; ++i) + { + scheduler_.Schedule( + new callback::CallbackValue1( + i, AddValueInOrder), 1); + expected.push_back(i); + } + + for (int i = 0; i < kThreadTestIteration; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } + EXPECT_THAT(ordered_value_, Eq(expected)); +} + +TEST_F(SchedulerTest, TriggerOrderDifferentDelay) { + std::vector expected; + for (int i = 0; i < 1000; ++i) + { + scheduler_.Schedule( + new callback::CallbackValue1( + i, AddValueInOrder), i); + expected.push_back(i); + } + + for (int i = 0; i < 1000; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(2000)); + } + + EXPECT_THAT(ordered_value_, Eq(expected)); +} + +TEST_F(SchedulerTest, ExecuteDuringCallback) { + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, [](Scheduler* scheduler){ + callback_sem1_.Post(); + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, [](Scheduler* scheduler){ + callback_sem2_.Post(); + })); + })); + + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(callback_sem2_.TimedWait(1000)); +} + +TEST_F(SchedulerTest, ScheduleDuringCallback1) { + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, [](Scheduler* scheduler){ + callback_sem1_.Post(); + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, [](Scheduler* scheduler){ + callback_sem2_.Post(); + }), 1); + }), 1); + + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(callback_sem2_.TimedWait(1000)); +} + +TEST_F(SchedulerTest, ScheduleDuringCallback100) { + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, [](Scheduler* scheduler){ + callback_sem1_.Post(); + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, [](Scheduler* scheduler){ + callback_sem2_.Post(); + }), 100); + }), 100); + + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(callback_sem2_.TimedWait(1000)); +} + +TEST_F(SchedulerTest, RecursiveCallbackNoInterval) { + repeat_period_ms_ = 0; + repeat_countdown_ = 1000; + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, RecursiveCallback), + repeat_period_ms_); + + for (int i = 0; i < 1000; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, RecursiveCallbackWithInterval) { + repeat_period_ms_ = 10; + repeat_countdown_ = 5; + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, RecursiveCallback), + repeat_period_ms_); + + for (int i = 0; i < 5; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, RepeatCallbackNoDelay) { + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1), 0, 1); + + // Wait for it to repeat 100 times + for (int i = 0; i < 100; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, RepeatCallbackWithDelay) { + int delay = 100; + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1), delay, 1); + + auto start = internal::GetTimestamp(); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + auto end = internal::GetTimestamp(); + + // Test if the first delay actually works. + int actual_delay = static_cast(end - start); + int error = abs(actual_delay - delay); + printf("Delay: %dms. Actual delay: %dms. Error: %dms\n", delay, actual_delay, + error); + EXPECT_TRUE(error < 0.1 * internal::kMillisecondsPerSecond); + + // Wait for it to repeat 100 times + for (int i = 0; i < 100; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, CancelImmediateCallback) { + auto test_func = [](int delay){ + // Use standalone scheduler and counter + Scheduler scheduler; + compat::Atomic count(0); + int success_cancel = 0; + for (int i = 0; i < kThreadTestIteration; ++i) { + bool cancelled = scheduler.Schedule( + new callback::CallbackValue1*>( + &count, [](compat::Atomic* count){ + count->fetch_add(1); + }), 0).Cancel(); + if (cancelled) { + ++success_cancel; + } + } + + internal::Sleep(10); + + // Does not guarantee 100% successful cancellation + float success_rate = success_cancel * 100.0f / kThreadTestIteration; + printf("[Delay %dms] Cancel success rate: %.1f%% (And it is ok if not 100%%" + ")\n", delay, success_rate); + EXPECT_THAT(success_cancel + count.load(), + Eq(kThreadTestIteration)); + }; + + // Test without delay + test_func(0); + + // Test with delay + test_func(1); +} + +// This test can take around 5s ~ 30s depending on the platform +TEST_F(SchedulerTest, CancelRepeatCallback) { + auto test_func = [](int delay, int repeat, int wait_repeat){ + // Use standalone scheduler and counter for iterations + Scheduler scheduler; + compat::Atomic count(0); + while (callback_sem1_.TryWait()) {} + + RequestHandle handler = + scheduler.Schedule(new callback::CallbackValue1*>( + &count, [](compat::Atomic* count){ + count->fetch_add(1); + callback_sem1_.Post(); + }), delay, repeat); + EXPECT_FALSE(handler.IsCancelled()); + + for (int i = 0; i < wait_repeat; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(handler.IsTriggered()); + } + + // Cancellation of a repeat cb should always be successful, as long as + // it is not cancelled yet + EXPECT_TRUE(handler.Cancel()); + EXPECT_TRUE(handler.IsCancelled()); + EXPECT_FALSE(handler.Cancel()); + + // Should have no more cb triggered after the cancellation + int saved_count = count.load(); + + internal::Sleep(1); + EXPECT_THAT(count.load(), Eq(saved_count)); + }; + + for (int i = 0; i < 1000; ++i) { + // No delay and do not wait for the first trigger to cancel it + test_func(0, 1, 0); + // No delay and wait for the first trigger, then cancel it + test_func(0, 1, 1); + // 1ms delay and do not wait for the first trigger to cancel it + test_func(1, 1, 0); + // 1ms delay and wait for the first trigger, then cancel it + test_func(1, 1, 1); + } +} + +TEST_F(SchedulerTest, CancelAll) { + Scheduler scheduler; + for (int i = 0; i < kThreadTestIteration; ++i) { + scheduler.Schedule(new callback::CallbackVoid(AddCount)); + } + scheduler.CancelAllAndShutdownWorkerThread(); + // Does not guarantee 0% trigger rate + float trigger_rate = atomic_count_.load() * 100.0f / kThreadTestIteration; + printf("Callback trigger rate: %.1f%% (And it is ok if not 0%%)\n", + trigger_rate); +} + +TEST_F(SchedulerTest, DeleteScheduler) { + for (int i = 0; i < kThreadTestIteration; ++i) { + Scheduler scheduler; + scheduler.Schedule(new callback::CallbackVoid(AddCount)); + } + + // Does not guarantee 0% trigger rate + float trigger_rate = atomic_count_.load() * 100.0f / kThreadTestIteration; + printf("Callback trigger rate: %.1f%% (And it is ok if not 0%%)\n", + trigger_rate); +} + +} // namespace scheduler +} // namespace firebase diff --git a/app/tests/secure/user_secure_integration_test.cc b/app/tests/secure/user_secure_integration_test.cc new file mode 100644 index 0000000000..7b6a851a46 --- /dev/null +++ b/app/tests/secure/user_secure_integration_test.cc @@ -0,0 +1,255 @@ +// Copyright 2019 Google LLC +// +// 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 // NOLINT +#include + +#include "app/src/secure/user_secure_internal.h" +#include "app/src/secure/user_secure_manager.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif // __APPLE__ + +// If FORCE_FAKE_SECURE_STORAGE is defined, force usage of fake (non-secure) +// storage, suitable for testing only, NOT for production use. Otherwise, use +// the default secure storage type for each platform, except on Linux if not +// running locally, which also forces fake storage (as libsecret requires that +// you are running locally), or on unknown other platforms (as there is no +// platform-independent secure storage solution). + +#if !defined(FORCE_FAKE_SECURE_STORAGE) +#if defined(_WIN32) +#include "app/src/secure/user_secure_windows_internal.h" +#define USER_SECURE_TYPE UserSecureWindowsInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(TARGET_OS_OSX) && TARGET_OS_OSX +#include "app/src/secure/user_secure_darwin_internal.h" +#include "app/src/secure/user_secure_darwin_internal_testlib.h" +#define USER_SECURE_TYPE UserSecureDarwinInternal +#define USER_SECURE_TEST_HELPER UserSecureDarwinTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(__linux__) && defined(USER_SECURE_LOCAL_TEST) +#include "app/src/secure/user_secure_linux_internal.h" +#define USER_SECURE_TYPE UserSecureLinuxInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#else // Unknown platform, or linux test running non-locally, use fake version. +#define FORCE_FAKE_SECURE_STORAGE +#endif // platform selector +#endif // !defined(FORCE_FAKE_SECURE_STORAGE) + +#ifdef FORCE_FAKE_SECURE_STORAGE +#include "app/src/secure/user_secure_fake_internal.h" +#define USER_SECURE_TYPE UserSecureFakeInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE GetTestTmpDir(kTestNameSpaceShort).c_str() +#if defined(_WIN32) +// For GetEnvironmentVariable to read TEST_TEMPDIR. +#include +#else +#include +#endif // defined(_WIN32) +#endif // FORCE_FAKE_SECURE_STORAGE + +namespace firebase { +namespace app { +namespace secure { + +using ::testing::Eq; +using ::testing::StrEq; + +class UserSecureEmptyTestHelper {}; + +#if defined(_WIN32) +static const char kDirectorySeparator[] = "\\"; +#else +static const char kDirectorySeparator[] = "/"; +#endif // defined(_WIN32) + +static std::string GetTestTmpDir(const char test_namespace[]) { +#if defined(_WIN32) + char buf[MAX_PATH + 1]; + if (GetEnvironmentVariable("TEST_TMPDIR", buf, sizeof(buf))) { + return std::string(buf) + kDirectorySeparator + test_namespace; + } +#else + // Linux and OS X should either have the TEST_TMPDIR environment variable set. + if (const char* value = getenv("TEST_TMPDIR")) { + return std::string(value) + kDirectorySeparator + test_namespace; + } +#endif // defined(_WIN32) + // If we weren't able to get TEST_TMPDIR, just use a subdirectory. + return test_namespace; +} + +// test app name and data +const char kAppName1[] = "app1"; +const char kUserData1[] = "123456"; +const char kAppName2[] = "app2"; +const char kUserData2[] = "654321"; + +const char kDomain[] = "integration_test"; + +// NOLINTNEXTLINE +const char kTestNameSpace[] = "com.google.firebase.TestKeys"; +// NOLINTNEXTLINE +const char kTestNameSpaceShort[] = "firebase_test"; + +class UserSecureTest : public ::testing::Test { + protected: + void SetUp() override { + user_secure_test_helper_ = MakeUnique(); + UserSecureInternal* internal = + new USER_SECURE_TYPE(kDomain, USER_SECURE_TEST_NAMESPACE); + UniquePtr user_secure_ptr(internal); + manager_ = new UserSecureManager(std::move(user_secure_ptr)); + CleanUpTestData(); + } + + void TearDown() override { + CleanUpTestData(); + delete manager_; + } + + void CleanUpTestData() { + Future delete_all_future = manager_->DeleteAllData(); + WaitForResponse(delete_all_future); + user_secure_test_helper_ = nullptr; + } + + // Busy waits until |response_future| has completed. + void WaitForResponse(const FutureBase& response_future) { + while (true) { + if (response_future.status() != FutureStatus::kFutureStatusPending) { + break; + } + } + } + + UserSecureManager* manager_; + UniquePtr user_secure_test_helper_; +}; + +TEST_F(UserSecureTest, NoData) { + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kNoEntry); + EXPECT_THAT(*(load_future.result()), StrEq("")); +} + +TEST_F(UserSecureTest, SetDataGetData) { + // Add Data + Future save_future = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future); + EXPECT_THAT(save_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future.error(), kSuccess); + // Check the added key for correctness + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kSuccess); + std::string originalString(kUserData1); + EXPECT_THAT(*(load_future.result()), StrEq(originalString)); +} + +TEST_F(UserSecureTest, SetDataDeleteDataGetNoData) { + // Add Data + Future save_future = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future); + EXPECT_THAT(save_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future.error(), kSuccess); + // Delete Data + Future delete_future = manager_->DeleteUserData(kAppName1); + WaitForResponse(delete_future); + EXPECT_THAT(delete_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(delete_future.error(), kSuccess); + // Check data empty + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kNoEntry); + EXPECT_THAT(*(load_future.result()), StrEq("")); +} + +TEST_F(UserSecureTest, SetTwoDataDeleteOneGetData) { + // Add Data1 + Future save_future1 = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future1); + EXPECT_THAT(save_future1.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future1.error(), kSuccess); + // Add Data2 + Future save_future2 = manager_->SaveUserData(kAppName2, kUserData2); + WaitForResponse(save_future2); + EXPECT_THAT(save_future2.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future2.error(), kSuccess); + + // Delete Data1 + Future delete_future = manager_->DeleteUserData(kAppName1); + WaitForResponse(delete_future); + EXPECT_THAT(delete_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(delete_future.error(), kSuccess); + + // Check the data2 + Future load_future = manager_->LoadUserData(kAppName2); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kSuccess); + std::string originalString(kUserData2); + EXPECT_THAT(*(load_future.result()), StrEq(originalString)); +} + +TEST_F(UserSecureTest, CheckDeleteAll) { + // Add Data1 + Future save_future1 = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future1); + EXPECT_THAT(save_future1.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future1.error(), kSuccess); + // Add Data2 + Future save_future2 = manager_->SaveUserData(kAppName2, kUserData2); + WaitForResponse(save_future2); + EXPECT_THAT(save_future2.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future2.error(), kSuccess); + + // Delete all data + Future delete_all_future = manager_->DeleteAllData(); + WaitForResponse(delete_all_future); + EXPECT_THAT(delete_all_future.status(), + Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(delete_all_future.error(), kSuccess); + // Check data1 empty + Future load_future1 = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future1); + EXPECT_THAT(load_future1.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future1.error(), kNoEntry); + EXPECT_THAT(*(load_future1.result()), StrEq("")); + + // Check data2 empty + Future load_future2 = manager_->LoadUserData(kAppName2); + WaitForResponse(load_future2); + EXPECT_THAT(load_future2.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future2.error(), kNoEntry); + EXPECT_THAT(*(load_future2.result()), StrEq("")); +} + +} // namespace secure +} // namespace app +} // namespace firebase diff --git a/app/tests/secure/user_secure_internal_test.cc b/app/tests/secure/user_secure_internal_test.cc new file mode 100644 index 0000000000..42f6a9c192 --- /dev/null +++ b/app/tests/secure/user_secure_internal_test.cc @@ -0,0 +1,285 @@ +// Copyright 2019 Google LLC +// +// 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 "gtest/gtest.h" +#include "gmock/gmock.h" + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif // __APPLE__ + +// If FORCE_FAKE_SECURE_STORAGE is defined, force usage of fake (non-secure) +// storage, suitable for testing only, NOT for production use. Otherwise, use +// the default secure storage type for each platform, except on Linux if not +// running locally, which also forces fake storage (as libsecret requires that +// you are running locally), or on unknown other platforms (as there is no +// platform-independent secure storage solution). + +#if !defined(FORCE_FAKE_SECURE_STORAGE) +#if defined(_WIN32) +#include "app/src/secure/user_secure_windows_internal.h" +#define USER_SECURE_TYPE UserSecureWindowsInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(TARGET_OS_OSX) && TARGET_OS_OSX +#include "app/src/secure/user_secure_darwin_internal.h" +#include "app/src/secure/user_secure_darwin_internal_testlib.h" +#define USER_SECURE_TYPE UserSecureDarwinInternal +#define USER_SECURE_TEST_HELPER UserSecureDarwinTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(__linux__) && defined(USER_SECURE_LOCAL_TEST) +#include "app/src/secure/user_secure_linux_internal.h" +#define USER_SECURE_TYPE UserSecureLinuxInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#else // Unknown platform, or linux test running non-locally, use fake version. +#define FORCE_FAKE_SECURE_STORAGE +#endif // platform selector +#endif // !defined(FORCE_FAKE_SECURE_STORAGE) + +#ifdef FORCE_FAKE_SECURE_STORAGE +#include "app/src/secure/user_secure_fake_internal.h" +#define USER_SECURE_TYPE UserSecureFakeInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE GetTestTmpDir(kTestNameSpaceShort).c_str() +#if defined(_WIN32) +// For GetEnvironmentVariable to read TEST_TEMPDIR. +#include +#else +#include +#endif // defined(_WIN32) +#endif // FORCE_FAKE_SECURE_STORAGE + +namespace firebase { +namespace app { +namespace secure { + +using ::testing::StrEq; + +class UserSecureEmptyTestHelper {}; + +#if defined(_WIN32) +static const char kDirectorySeparator[] = "\\"; +#else +static const char kDirectorySeparator[] = "/"; +#endif // defined(_WIN32) + +static std::string GetTestTmpDir(const char test_namespace[]) { +#if defined(_WIN32) + char buf[MAX_PATH + 1]; + if (GetEnvironmentVariable("TEST_TMPDIR", buf, sizeof(buf))) { + return std::string(buf) + kDirectorySeparator + test_namespace; + } +#else + // Linux and OS X should either have the TEST_TMPDIR environment variable set. + if (const char* value = getenv("TEST_TMPDIR")) { + return std::string(value) + kDirectorySeparator + test_namespace; + } +#endif // defined(_WIN32) + // If we weren't able to get TEST_TMPDIR, just use a subdirectory. + return test_namespace; +} + +// test app name and data +const char kAppName1[] = "app1"; +const char kUserData1[] = "123456"; +const char kUserData1Alt[] = "12345ABC"; +const char kUserData1ReAdd[] = "123456789"; +const char kAppName2[] = "app2"; +const char kUserData2[] = "654321"; +const char kAppNameNoExist[] = "app_no_exist"; + +const char kDomain[] = "internal_test"; + +// NOLINTNEXTLINE +const char kTestNameSpace[] = "com.google.firebase.TestKeys"; +// NOLINTNEXTLINE +const char kTestNameSpaceShort[] = "firebase_test"; + +class UserSecureInternalTest : public ::testing::Test { + protected: + void SetUp() override { + user_secure_test_helper_ = MakeUnique(); + user_secure_ = + MakeUnique(kDomain, USER_SECURE_TEST_NAMESPACE); + CleanUpTestData(); + } + + void TearDown() override { + CleanUpTestData(); + user_secure_ = nullptr; + user_secure_test_helper_ = nullptr; + } + + void CleanUpTestData() { user_secure_->DeleteAllData(); } + + UniquePtr user_secure_; + UniquePtr user_secure_test_helper_; +}; + +TEST_F(UserSecureInternalTest, NoData) { + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetDataGetData) { + // Add Data + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check the added key for correctness + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); +} + +TEST_F(UserSecureInternalTest, SetDataDeleteDataGetNoData) { + // Add Data + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Delete Data + user_secure_->DeleteUserData(kAppName1); + // Check data empty + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetTwoDataDeleteOneGetData) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Add Data2 + user_secure_->SaveUserData(kAppName2, kUserData2); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); + // Check previous save is still valid. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Delete Data1 + user_secure_->DeleteUserData(kAppName1); + // Check the data2 + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); +} + +TEST_F(UserSecureInternalTest, CheckDeleteAll) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Add Data2 + user_secure_->SaveUserData(kAppName2, kUserData2); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); + // Delete all data + user_secure_->DeleteAllData(); + // Check data1 empty + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); + // Check data2 empty + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetGetAfterDeleteAll) { + // Delete all data + user_secure_->DeleteAllData(); + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check data1 correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); +} + +TEST_F(UserSecureInternalTest, AddOverride) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check data1 correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Override same key with Data1ReAdd. + user_secure_->SaveUserData(kAppName1, kUserData1ReAdd); + // Check Data1ReAdd correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1ReAdd)); +} + +TEST_F(UserSecureInternalTest, DeleteAndAddWithSameKey) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check data1 correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Delete Data1 + user_secure_->DeleteUserData(kAppName1); + // Add Data1ReAdd to same key. + user_secure_->SaveUserData(kAppName1, kUserData1ReAdd); + // Check Data1ReAdd correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1ReAdd)); +} + +TEST_F(UserSecureInternalTest, DeleteKeyNotExist) { + // Delete Data1 + user_secure_->DeleteUserData(kAppNameNoExist); + // Check data1 empty + EXPECT_THAT(user_secure_->LoadUserData(kAppNameNoExist), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetLargeDataThenDeleteIt) { + // Set up a large buffer of data. + const size_t kSize = 20000; + char data[kSize]; + for (int i = 0; i < kSize - 1; ++i) { + data[i] = 'A' + (i % 26); + } + data[kSize - 1] = '\0'; + std::string user_data(data); + // Add Data + user_secure_->SaveUserData(kAppName1, user_data); + // Check the added key for correctness + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(user_data)); + // Check that we can delete the large data. + user_secure_->DeleteUserData(kAppName1); + // Check the added key for correctness + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); +} + +TEST_F(UserSecureInternalTest, TestMultipleDomains) { + // Set up an alternate UserSecureInternal with a different domain. + UniquePtr alt_user_secure = MakeUnique( + "alternate_test", USER_SECURE_TEST_NAMESPACE); + alt_user_secure->DeleteAllData(); + + user_secure_->SaveUserData(kAppName1, kUserData1); + user_secure_->SaveUserData(kAppName2, kUserData2); + alt_user_secure->SaveUserData(kAppName1, kUserData1Alt); + + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)) + << "Modifying a key in alt_user_secure changed a key in user_secure_"; + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName1), StrEq(kUserData1Alt)); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName2), StrEq("")); + + // Ensure deleting data from one UserSecureInternal doesn't delete data in the + // other. + alt_user_secure->DeleteUserData(kAppName1); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName1), StrEq("")); + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + + alt_user_secure->SaveUserData(kAppName1, kUserData1Alt); + alt_user_secure->SaveUserData(kAppName2, kUserData2); + // Ensure deleting ALL data from one UserSecureInternal doesn't delete the + // other. + alt_user_secure->DeleteAllData(); + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName1), StrEq("")); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName2), StrEq("")); +} + +} // namespace secure +} // namespace app +} // namespace firebase diff --git a/app/tests/secure/user_secure_manager_test.cc b/app/tests/secure/user_secure_manager_test.cc new file mode 100644 index 0000000000..d3f1864e08 --- /dev/null +++ b/app/tests/secure/user_secure_manager_test.cc @@ -0,0 +1,178 @@ +// Copyright 2019 Google LLC +// +// 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 "app/src/secure/user_secure_manager.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace app { +namespace secure { + +using ::testing::Ne; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::StrEq; + +const char kAppName1[] = "app_name_1"; +const char kUserData1[] = "123456"; + +TEST(UserSecureManager, Constructor) { + UniquePtr user_secure; + UserSecureManager manager(std::move(user_secure)); + + // Just making sure this constructor doesn't crash or leak memory. No further + // tests. +} + +class UserSecureInternalMock : public UserSecureInternal { + public: + MOCK_METHOD(std::string, LoadUserData, (const std::string& app_name), + (override)); + MOCK_METHOD(void, SaveUserData, + (const std::string& app_name, const std::string& user_data), + (override)); + MOCK_METHOD(void, DeleteUserData, (const std::string& app_name), (override)); + MOCK_METHOD(void, DeleteAllData, (), (override)); +}; + +class UserSecureManagerTest : public ::testing::Test { + public: + friend class UserSecureManager; + void SetUp() override { + user_secure_ = new testing::StrictMock(); + UniquePtr user_secure_ptr(user_secure_); + + manager_ = new UserSecureManager(std::move(user_secure_ptr)); + } + + void TearDown() override { delete manager_; } + + // Busy waits until |response_future| has completed. + void WaitForResponse(const FutureBase& response_future) { + ASSERT_THAT(response_future.status(), + Ne(FutureStatus::kFutureStatusInvalid)); + while (true) { + if (response_future.status() != FutureStatus::kFutureStatusPending) { + break; + } + } + } + + protected: + UserSecureInternalMock* user_secure_; + UserSecureManager* manager_; +}; + +TEST_F(UserSecureManagerTest, LoadUserData) { + EXPECT_CALL(*user_secure_, LoadUserData(kAppName1)) + .WillOnce(Return(kUserData1)); + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_EQ(load_future.status(), FutureStatus::kFutureStatusComplete); + EXPECT_THAT(load_future.result(), Pointee(StrEq(kUserData1))); +} + +TEST_F(UserSecureManagerTest, SaveUserData) { + EXPECT_CALL(*user_secure_, SaveUserData(kAppName1, kUserData1)).Times(1); + Future save_future = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future); + EXPECT_EQ(save_future.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(UserSecureManagerTest, DeleteUserData) { + EXPECT_CALL(*user_secure_, DeleteUserData(kAppName1)).Times(1); + Future delete_future = manager_->DeleteUserData(kAppName1); + WaitForResponse(delete_future); + EXPECT_EQ(delete_future.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(UserSecureManagerTest, DeleteAllData) { + EXPECT_CALL(*user_secure_, DeleteAllData()).Times(1); + Future delete_all_future = manager_->DeleteAllData(); + WaitForResponse(delete_all_future); + + EXPECT_EQ(delete_all_future.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(UserSecureManagerTest, TestHexEncodingAndDecoding) { + const char kBinaryData[] = + "\x00\x05\x20\x3C\x40\x45\x50\x60\x70\x80\x90\x00\xA0\xB5\xC2\xD1\xF0" + "\xFF\x00\xE0\x42"; + const char kBase64EncodedData[] = "#AAUgPEBFUGBwgJAAoLXC0fD/AOBC"; + const char kHexEncodedData[] = "$0005203C4045506070809000A0B5C2D1F0FF00E042"; + std::string binary_data(kBinaryData, sizeof(kBinaryData) - 1); + std::string encoded; + std::string decoded; + + UserSecureManager::BinaryToAscii(binary_data, &encoded); + // Ensure that the data was Base64-encoded. + EXPECT_EQ(encoded, kBase64EncodedData); + // Ensure the data decodes back to the original. + EXPECT_TRUE(UserSecureManager::AsciiToBinary(encoded, &decoded)); + EXPECT_EQ(decoded, binary_data); + + // Explicitly check decoding from hex and from base64. + { + std::string decoded_from_hex; + EXPECT_TRUE( + UserSecureManager::AsciiToBinary(kHexEncodedData, &decoded_from_hex)); + EXPECT_EQ(decoded_from_hex, binary_data); + } + { + std::string decoded_from_base64; + EXPECT_TRUE(UserSecureManager::AsciiToBinary(kBase64EncodedData, + &decoded_from_base64)); + EXPECT_EQ(decoded_from_base64, binary_data); + } + + // Test encoding and decoding empty strings. + std::string empty; + UserSecureManager::BinaryToAscii("", &empty); + EXPECT_EQ(empty, "#"); + EXPECT_TRUE(UserSecureManager::AsciiToBinary("#", &empty)); + EXPECT_EQ(empty, ""); + EXPECT_TRUE(UserSecureManager::AsciiToBinary("$", &empty)); + EXPECT_EQ(empty, ""); + + std::string u; // unused + + // Bad hex encodings. + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$11223", &u)); // odd size after header + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("ABCDEF01", &u)); // missing header + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A2GB34F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A:23A4F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A23A4$F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A2BG34F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A2:3A4F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A23A4F!", &u)); // bad characters + + // Bad base64 encodings. + EXPECT_FALSE(UserSecureManager::AsciiToBinary("#*", &u)); // invalid base64 + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("#AAAA#AAAA", &u)); // bad characters +} + +} // namespace secure +} // namespace app +} // namespace firebase diff --git a/app/tests/semaphore_test.cc b/app/tests/semaphore_test.cc new file mode 100644 index 0000000000..a3262cc33f --- /dev/null +++ b/app/tests/semaphore_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/semaphore.h" + +#include "app/src/thread.h" +#include "app/src/time.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +// Basic test of TryWait, to make sure that its successes and failures +// line up with what we'd expect, based on the initial count. +TEST(SemaphoreTest, TryWaitTests) { + firebase::Semaphore sem(2); + + // First time, should be able to get a value just fine. + EXPECT_EQ(sem.TryWait(), true); + + // Second time, should still be able to get a value. + EXPECT_EQ(sem.TryWait(), true); + + // Second time, we should be unable to acquire a lock. + EXPECT_EQ(sem.TryWait(), false); + + sem.Post(); + + // Should be able to get a lock now. + EXPECT_EQ(sem.TryWait(), true); +} + +// Test that semaphores work across threads. +// Blocks, after setting a thread to unlock itself in 1 second. +// If the thread doesn't unblock it, it will wait forever, triggering a test +// failure via timeout after 60 seconds, through the testing framework. +TEST(SemaphoreTest, MultithreadedTest) { + firebase::Semaphore sem(0); + + firebase::Thread( + [](void* data_) { + auto sem = static_cast(data_); + firebase::internal::Sleep(firebase::internal::kMillisecondsPerSecond); + sem->Post(); + }, + &sem) + .Detach(); + + // This will block, until the thread releases it. + sem.Wait(); +} + +// Tests that Timed Wait works as intended. +TEST(SemaphoreTest, TimedWait) { + firebase::Semaphore sem(0); + + int64_t start_ms = firebase::internal::GetTimestamp(); + EXPECT_FALSE(sem.TimedWait(firebase::internal::kMillisecondsPerSecond)); + int64_t finish_ms = firebase::internal::GetTimestamp(); + + assert(labs((finish_ms - start_ms) - + firebase::internal::kMillisecondsPerSecond) < + 0.10 * firebase::internal::kMillisecondsPerSecond); +} + +TEST(SemaphoreTest, DISABLED_MultithreadedStressTest) { + for (int i = 0; i < 10000; ++i) { + firebase::Semaphore sem(0); + + firebase::Thread thread = firebase::Thread( + [](void* data_) { + auto sem = static_cast(data_); + sem->Post(); + }, + &sem); + // This will block, until the thread releases it or it times out. + EXPECT_TRUE(sem.TimedWait(100)); + + thread.Join(); + } +} + +} // namespace diff --git a/app/tests/swizzle_test.mm b/app/tests/swizzle_test.mm new file mode 100644 index 0000000000..72f6b192c5 --- /dev/null +++ b/app/tests/swizzle_test.mm @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#import +#import +#include "app/src/util_ios.h" + +@interface SwizzlingTests : XCTestCase +@end + +@interface AppDelegate : UIResponder +@property(strong, nonatomic) NSMutableArray *selectorList; +@end + +@implementation AppDelegate + +- (instancetype)init { + if (self = [super init]) { + _selectorList = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + // Save the selectors and arguments that were called this way, to validate against later. + const char *selName = sel_getName([invocation selector]); + NSMutableString *toAdd = [NSMutableString stringWithUTF8String:selName]; + int numArgs = [[invocation methodSignature] numberOfArguments]; + for (int i = 2; i < numArgs; i++) { + __unsafe_unretained id arg; + [invocation getArgument:&arg atIndex:i]; + [toAdd appendString:[NSString stringWithFormat:@"|%p", arg]]; + } + [_selectorList addObject:toAdd]; +} + +@end + +@implementation SwizzlingTests + +- (void)testForwardInvocationPassThrough { + AppDelegate *appDelegate = [[AppDelegate alloc] init]; + + UIApplication *application = [UIApplication sharedApplication]; + NSURL *url = [[NSURL alloc] init]; + NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"myactivity"]; + void (^handler)(NSArray *); + NSData *data = [[NSData alloc] init]; + NSError *error = [[NSError alloc] init]; + firebase::util::UIBackgroundFetchResultFunction fetchHandler; + NSDictionary *dict = [[NSDictionary alloc] init]; + NSString *string = @"TestString"; + id testId = data; + + NSMutableArray *expectedList = [[NSMutableArray alloc] init]; + + // From invites_ios_startup.mm + [expectedList addObject:[NSString stringWithFormat:@"application:openURL:options:|%p|%p|%p", + application, url, dict]]; + [appDelegate application:application openURL:url options:dict]; + + [expectedList + addObject:[NSString stringWithFormat: + @"application:openURL:sourceApplication:annotation:|%p|%p|%p|%p", + application, url, string, testId]]; + [appDelegate application:application openURL:url sourceApplication:string annotation:testId]; + + [expectedList + addObject:[NSString stringWithFormat: + @"application:continueUserActivity:restorationHandler:|%p|%p|%p", + application, activity, handler]]; + [appDelegate application:application continueUserActivity:activity restorationHandler:handler]; + + [expectedList + addObject:[NSString stringWithFormat:@"applicationDidBecomeActive:|%p", application]]; + [appDelegate applicationDidBecomeActive:application]; + + // From instance_id.mm + [expectedList + addObject:[NSString + stringWithFormat: + @"application:didRegisterForRemoteNotificationsWithDeviceToken:|%p|%p", + application, data]]; + [appDelegate application:application didRegisterForRemoteNotificationsWithDeviceToken:data]; + + // From messaging.mm + [expectedList + addObject:[NSString stringWithFormat:@"application:didFinishLaunchingWithOptions:|%p|%p", + application, dict]]; + [appDelegate application:application didFinishLaunchingWithOptions:dict]; + + [expectedList + addObject:[NSString stringWithFormat:@"applicationDidEnterBackground:|%p", application]]; + [appDelegate applicationDidEnterBackground:application]; + + [expectedList + addObject:[NSString + stringWithFormat: + @"application:didFailToRegisterForRemoteNotificationsWithError:|%p|%p", + application, error]]; + [appDelegate application:application didFailToRegisterForRemoteNotificationsWithError:error]; + + [expectedList + addObject:[NSString stringWithFormat:@"application:didReceiveRemoteNotification:|%p|%p", + application, dict]]; + [appDelegate application:application didReceiveRemoteNotification:dict]; + + [expectedList addObject:[NSString stringWithFormat:@"application:didReceiveRemoteNotification:" + @"fetchCompletionHandler:|%p|%p|%p", + application, dict, fetchHandler]]; + [appDelegate application:application + didReceiveRemoteNotification:dict + fetchCompletionHandler:fetchHandler]; + + XCTAssertEqualObjects([appDelegate selectorList], expectedList); +} + +@end diff --git a/app/tests/thread_test.cc b/app/tests/thread_test.cc new file mode 100644 index 0000000000..9562d61388 --- /dev/null +++ b/app/tests/thread_test.cc @@ -0,0 +1,194 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/thread.h" + +#include "app/src/mutex.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +using ::testing::Eq; + +// Simple thread safe wrapper around a value T. +template +class ThreadSafe { + public: + explicit ThreadSafe(T value) : value_(value) {} + + T get() const { + firebase::MutexLock lock(const_cast(mtx_)); + return value_; + } + + void set(const T& value) { + firebase::MutexLock lock(mtx_); + value_ = value; + } + + private: + T value_; + firebase::Mutex mtx_; +}; + +TEST(ThreadTest, ThreadExecutesAndJoinWaitsForItToFinish) { + ThreadSafe value(false); + + firebase::Thread thread([](ThreadSafe* value) { value->set(true); }, + &value); + thread.Join(); + + ASSERT_THAT(value.get(), Eq(true)); +} + +TEST(ThreadTest, ThreadIsNotJoinableAfterJoin) { + firebase::Thread thread([] {}); + ASSERT_THAT(thread.Joinable(), Eq(true)); + + thread.Join(); + ASSERT_THAT(thread.Joinable(), Eq(false)); +} + +TEST(ThreadTest, ThreadIsNotJoinableAfterDetach) { + firebase::Thread thread([] {}); + ASSERT_THAT(thread.Joinable(), Eq(true)); + + thread.Detach(); + ASSERT_THAT(thread.Joinable(), Eq(false)); +} + +TEST(ThreadTest, ThreadShouldNotBeJoinableAfterBeingMoveAssignedOutOf) { + firebase::Thread source([] {}); + firebase::Thread target; + + ASSERT_THAT(source.Joinable(), Eq(true)); + + // cast due to lack of std::move in STLPort + target = static_cast(source); + ASSERT_THAT(source.Joinable(), Eq(false)); + ASSERT_THAT(target.Joinable(), Eq(true)); + target.Join(); +} + +TEST(ThreadTest, ThreadShouldNotBeJoinableAfterBeingMoveFrom) { + firebase::Thread source([] {}); + + ASSERT_THAT(source.Joinable(), Eq(true)); + + // cast due to lack of std::move in STLPort + firebase::Thread target(static_cast(source)); + ASSERT_THAT(source.Joinable(), Eq(false)); + ASSERT_THAT(target.Joinable(), Eq(true)); + target.Join(); +} + +TEST(ThreadDeathTest, MovingIntoRunningThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread = firebase::Thread(); + }, + ""); +} + +TEST(ThreadDeathTest, JoinEmptyThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread; + thread.Join(); + }, + ""); +} + +TEST(ThreadDeathTest, JoinThreadMultipleTimesShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Join(); + thread.Join(); + }, + ""); +} + +TEST(ThreadDeathTest, JoinDetachedThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Detach(); + thread.Join(); + }, + ""); +} + +TEST(ThreadDeathTest, DetachJoinedThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Join(); + thread.Detach(); + }, + ""); +} + +TEST(ThreadDeathTest, DetachEmptyThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread; + + thread.Detach(); + }, + ""); +} + +TEST(ThreadDeathTest, DetachThreadMultipleTimesShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Detach(); + thread.Detach(); + }, + ""); +} + +TEST(ThreadDeathTest, WhenJoinableThreadIsDestructedShouldAbort) { + ASSERT_DEATH({ firebase::Thread thread([] {}); }, ""); +} + +TEST(ThreadTest, ThreadIsEqualToItself) { + firebase::Thread::Id thread_id = firebase::Thread::CurrentId(); + ASSERT_THAT(firebase::Thread::IsCurrentThread(thread_id), Eq(true)); +} + +TEST(ThreadTest, ThreadIsNotEqualToDifferentThread) { + ThreadSafe value(firebase::Thread::CurrentId()); + + firebase::Thread thread( + [](ThreadSafe* value) { + value->set(firebase::Thread::CurrentId()); + }, &value); + thread.Join(); + + ASSERT_THAT(firebase::Thread::IsCurrentThread(value.get()), Eq(false)); +} + +} // namespace diff --git a/app/tests/time_test.cc b/app/tests/time_test.cc new file mode 100644 index 0000000000..9c768cdf2d --- /dev/null +++ b/app/tests/time_test.cc @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/time.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +#ifndef WIN32 +// Test that the normalize function works, for timespecs +TEST(TimeTests, NormalizeTest) { + timespec t; + t.tv_sec = 2; + t.tv_nsec = firebase::internal::kNanosecondsPerSecond * 5.5; + firebase::internal::NormalizeTimespec(&t); + + EXPECT_EQ(t.tv_sec, 7); + EXPECT_EQ(t.tv_nsec, firebase::internal::kNanosecondsPerSecond * 0.5); +} + +// Test the various conversions to and from timespecs. +TEST(TimeTests, ConversionTests) { + timespec t; + + // Test that we can convert timespecs into milliseconds. + t.tv_sec = 2; + t.tv_nsec = firebase::internal::kNanosecondsPerSecond * 0.5; + EXPECT_EQ(firebase::internal::TimespecToMs(t), 2500); + + // Test conversion of milliseconds into timespecs. + t = firebase::internal::MsToTimespec(6789); + EXPECT_EQ(t.tv_sec, 6); + EXPECT_EQ(t.tv_nsec, 789 * firebase::internal::kNanosecondsPerMillisecond); +} + +// Test the timespec compare function. +TEST(TimeTests, ComparisonTests) { + timespec t1, t2; + clock_gettime(CLOCK_REALTIME, &t1); + firebase::internal::Sleep(500); + clock_gettime(CLOCK_REALTIME, &t2); + + EXPECT_EQ(firebase::internal::TimespecCmp(t1, t2), -1); + EXPECT_EQ(firebase::internal::TimespecCmp(t2, t1), 1); + EXPECT_EQ(firebase::internal::TimespecCmp(t1, t1), 0); + EXPECT_EQ(firebase::internal::TimespecCmp(t2, t2), 0); +} +#endif + +// Test GetTimestamp function +TEST(TimeTests, GetTimestampTest) { + uint64_t start = firebase::internal::GetTimestamp(); + + firebase::internal::Sleep(500); + + uint64_t end = firebase::internal::GetTimestamp(); + + int64_t error = llabs(static_cast(end - start) - 500); + + EXPECT_TRUE(error < 0.10 * firebase::internal::kMillisecondsPerSecond); +} + +// Test GetTimestampEpoch function +TEST(TimeTests, GetTimestampEpochTest) { + uint64_t start = firebase::internal::GetTimestampEpoch(); + + firebase::internal::Sleep(500); + + uint64_t end = firebase::internal::GetTimestampEpoch(); + + int64_t error = llabs(static_cast(end - start) - 500); + + // Print out the epoch time so that we can verify the timestamp from the log + // This is the easiest way to verify if the function works in all platform +#ifdef __linux__ + printf("%lu -> %lu (%ld)\n", start, end, error); +#else + printf("%llu -> %llu (%lld)\n", start, end, error); +#endif // __linux__ + + EXPECT_TRUE(error < 0.10 * firebase::internal::kMillisecondsPerSecond); +} + +} // namespace diff --git a/app/tests/util_android_test.cc b/app/tests/util_android_test.cc new file mode 100644 index 0000000000..eeec59fa24 --- /dev/null +++ b/app/tests/util_android_test.cc @@ -0,0 +1,479 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/util_android.h" + +#include +#include + +#include "app/src/include/firebase/variant.h" +#include "app/src/semaphore.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/run_all_tests.h" + +namespace firebase { +namespace util { + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; +using ::testing::Ne; + +TEST(UtilAndroidTest, TestInitializeAndTerminate) { + // Initialize firebase util, including caching class/methods and dealing with + // embedded jar. + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + EXPECT_NE(nullptr, env); + jobject activity_object = firebase::testing::cppsdk::GetTestActivity(); + EXPECT_NE(nullptr, activity_object); + EXPECT_TRUE(Initialize(env, activity_object)); + + Terminate(env); +} + +TEST(JniUtilities, LocalToGlobalReference) { + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + jobject local_java_string = env->NewStringUTF("a string"); + jobject global_java_string = LocalToGlobalReference(env, local_java_string); + EXPECT_NE(nullptr, global_java_string); + env->DeleteGlobalRef(global_java_string); + + EXPECT_EQ(nullptr, LocalToGlobalReference(env, nullptr)); +} + +// Test execution on the main and background Java threads. +class JavaThreadContextTest : public ::testing::Test { + protected: + class ThreadContext { + public: + explicit ThreadContext(JavaThreadContext *java_thread_context = nullptr) + : started_(1), + complete_(1), + block_store_(1), + canceled_(false), + cancel_store_called_(false), + java_thread_context_(java_thread_context) { + thread_id_ = pthread_self(); + started_.Wait(); + complete_.Wait(); + block_store_.Wait(); + } + + // Wait for the thread to start. + void WaitForStart() { started_.Wait(); } + + // Wait for the thread to complete. + void WaitForCompletion() { complete_.Wait(); } + + // Continue Store() execution (if it's blocked). + void Continue() { block_store_.Post(); } + + // Get the thread ID. + pthread_t thread_id() const { return thread_id_; } + + // Get whether the thread was canceled. + bool canceled() const { return canceled_; } + + // Get whether CancelStore was called. + bool cancel_store_called() const { return cancel_store_called_; } + + // Store the current thread ID and signal thread completion. + static void Store(void *data) { + static_cast(data)->Store(false); + } + + // Wait for Continue() to be called then store the current thread ID + // if the context wasn't cancelled and signal thread completion. + static void WaitAndStore(void *data) { + static_cast(data)->Store(true); + } + + // Cancel the store operation. + static void CancelStore(void *data) { + static_cast(data)->cancel_store_called_ = true; + } + + private: + // Store the current thread ID if the object wasn't canceled and + // signal thread completion. + void Store(bool wait) { + if (wait) { + if (java_thread_context_) { + // Release the execution lock so the thread can be canceled. + java_thread_context_->ReleaseExecuteCancelLock(); + } + // Signal that the thread has started. + started_.Post(); + // Wait for Continue(). + block_store_.Wait(); + if (java_thread_context_) { + // If this method returns false, the thread is canceled. + canceled_ = !java_thread_context_->AcquireExecuteCancelLock(); + } + } else { + started_.Post(); + } + if (!canceled_) thread_id_ = pthread_self(); + complete_.Post(); + } + + private: + // ID of the thread. + pthread_t thread_id_; + // Signalled when the thread starts. + Semaphore started_; + // Signalled when a thread is complete. + Semaphore complete_; + // Used to block execution of Store(). + Semaphore block_store_; + // Whether the Store() operation was canceled. + bool canceled_; + // Whether CancelStore() was called. + bool cancel_store_called_; + JavaThreadContext *java_thread_context_; + }; + + void SetUp() override { + env_ = firebase::testing::cppsdk::GetTestJniEnv(); + ASSERT_TRUE(env_ != nullptr); + activity_ = firebase::testing::cppsdk::GetTestActivity(); + ASSERT_TRUE(activity_ != nullptr); + ASSERT_TRUE(Initialize(env_, activity_)); + } + + void TearDown() override { + ASSERT_TRUE(env_ != nullptr); + Terminate(env_); + } + + JNIEnv *env_; + jobject activity_; +}; + +TEST_F(JavaThreadContextTest, RunOnMainThread) { + ThreadContext thread_context(nullptr); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnMainThread(env_, activity_, ThreadContext::Store, &thread_context); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Ne(main_thread_id)); +} + +TEST_F(JavaThreadContextTest, RunOnMainThreadAndCancel) { + JavaThreadContext java_thread_context(env_); + ThreadContext thread_context(&java_thread_context); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnMainThread(env_, activity_, ThreadContext::WaitAndStore, &thread_context, + ThreadContext::CancelStore, &java_thread_context); + thread_context.WaitForStart(); + java_thread_context.Cancel(); + thread_context.Continue(); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Eq(main_thread_id)); + EXPECT_TRUE(thread_context.canceled()); + EXPECT_TRUE(thread_context.cancel_store_called()); +} + +TEST_F(JavaThreadContextTest, RunOnBackgroundThread) { + ThreadContext thread_context(nullptr); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnBackgroundThread(env_, ThreadContext::Store, &thread_context); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Ne(main_thread_id)); +} + +TEST_F(JavaThreadContextTest, RunOnBackgroundThreadAndCancel) { + JavaThreadContext java_thread_context(env_); + ThreadContext thread_context(&java_thread_context); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnBackgroundThread(env_, ThreadContext::WaitAndStore, &thread_context, + ThreadContext::CancelStore, &java_thread_context); + thread_context.WaitForStart(); + java_thread_context.Cancel(); + thread_context.Continue(); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Eq(main_thread_id)); + EXPECT_TRUE(thread_context.canceled()); + EXPECT_TRUE(thread_context.cancel_store_called()); +} + +/***** JavaObjectToVariant test *****/ +class JavaObjectToVariantTest : public ::testing::Test { + protected: + void SetUp() override { + env_ = firebase::testing::cppsdk::GetTestJniEnv(); + ASSERT_TRUE(env_ != nullptr); + activity_ = firebase::testing::cppsdk::GetTestActivity(); + ASSERT_TRUE(activity_ != nullptr); + ASSERT_TRUE(Initialize(env_, activity_)); + } + + void TearDown() override { + ASSERT_TRUE(env_ != nullptr); + Terminate(env_); + } + + const int kTestValueInt = 0x01234567; + const int64 kTestValueLong = 0x1234567ABCD1234L; + const int16 kTestValueShort = 0x3456; + const char kTestValueByte = 0x12; + const bool kTestValueBool = true; + const char *const kTestValueString = "Hello, world!"; + const float kTestValueFloat = 0.15625f; + const double kTestValueDouble = 1048576.15625; + + JNIEnv *env_; + jobject activity_; +}; + +TEST_F(JavaObjectToVariantTest, TestFundamentalTypes) { + // null converts to Variant::kTypeNull. + { + jobject obj = nullptr; + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), Variant::Null()); + } + // Integral types convert to Variant::kTypeInt64. This includes Date. + { + // Integer + jobject obj = + env_->NewObject(integer_class::GetClass(), + integer_class::GetMethodId(integer_class::kConstructor), + static_cast(kTestValueInt)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueInt)) + << "Failed to convert Integer"; + env_->DeleteLocalRef(obj); + } + { + // Short + jobject obj = + env_->NewObject(short_class::GetClass(), + short_class::GetMethodId(short_class::kConstructor), + static_cast(kTestValueShort)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueShort)) + << "Failed to convert Short"; + env_->DeleteLocalRef(obj); + } + { + // Long + jobject obj = + env_->NewObject(long_class::GetClass(), + long_class::GetMethodId(long_class::kConstructor), + static_cast(kTestValueLong)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueLong)) + << "Failed to convert Long"; + env_->DeleteLocalRef(obj); + } + { + // Byte + jobject obj = + env_->NewObject(byte_class::GetClass(), + byte_class::GetMethodId(byte_class::kConstructor), + static_cast(kTestValueByte)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueByte)) + << "Failed to convert Byte"; + env_->DeleteLocalRef(obj); + } + { + // Date becomes an Int64 of milliseconds since epoch, which is also what the + // Java Date constructor happens to take as an argument. + jobject obj = env_->NewObject(date::GetClass(), + date::GetMethodId(date::kConstructorWithTime), + static_cast(kTestValueLong)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueLong)) + << "Failed to convert Date"; + env_->DeleteLocalRef(obj); + } + + // Floating point types convert to Variant::kTypeDouble. + { + // Float + jobject obj = + env_->NewObject(float_class::GetClass(), + float_class::GetMethodId(float_class::kConstructor), + static_cast(kTestValueFloat)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromDouble(kTestValueFloat)) + << "Failed to convert Float"; + env_->DeleteLocalRef(obj); + } + { + // Double + jobject obj = + env_->NewObject(double_class::GetClass(), + double_class::GetMethodId(double_class::kConstructor), + static_cast(kTestValueDouble)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromDouble(kTestValueDouble)) + << "Failed to convert Double"; + env_->DeleteLocalRef(obj); + } + // Boolean converts to Variant::kTypeBool. + { + jobject obj = + env_->NewObject(boolean_class::GetClass(), + boolean_class::GetMethodId(boolean_class::kConstructor), + static_cast(kTestValueBool)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromBool(kTestValueBool)) + << "Failed to convert Boolean"; + env_->DeleteLocalRef(obj); + } + // String converts to Variant::kTypeMutableString. + { + jobject obj = env_->NewStringUTF(kTestValueString); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromMutableString(kTestValueString)) + << "Failed to convert String"; + env_->DeleteLocalRef(obj); + } +} + +TEST_F(JavaObjectToVariantTest, TestContainerTypes) { + // Array and List types convert to Variant::kTypeVector. + { + // Two tests in one: Array of Objects, and ArrayList. + // Both contain {Integer, Float, String, Null}. + + jobjectArray array = env_->NewObjectArray(4, object::GetClass(), nullptr); + jobject container = + env_->NewObject(array_list::GetClass(), + array_list::GetMethodId(array_list::kConstructor)); + { + // Element 1: Integer + jobject obj = env_->NewObject( + integer_class::GetClass(), + integer_class::GetMethodId(integer_class::kConstructor), + static_cast(kTestValueInt)); + env_->SetObjectArrayElement(array, 0, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + env_->DeleteLocalRef(obj); + } + { + // Element 2: Float + jobject obj = + env_->NewObject(float_class::GetClass(), + float_class::GetMethodId(float_class::kConstructor), + static_cast(kTestValueFloat)); + env_->SetObjectArrayElement(array, 1, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + env_->DeleteLocalRef(obj); + } + { + // Element 3: String + jobject obj = env_->NewStringUTF(kTestValueString); + env_->SetObjectArrayElement(array, 2, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + env_->DeleteLocalRef(obj); + } + { + // Element 4: Null + jobject obj = nullptr; + env_->SetObjectArrayElement(array, 3, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + } + + Variant expected = Variant::EmptyVector(); + expected.vector().push_back(Variant::FromInt64(kTestValueInt)); + expected.vector().push_back(Variant::FromDouble(kTestValueFloat)); + expected.vector().push_back(Variant::FromMutableString(kTestValueString)); + expected.vector().push_back(Variant::Null()); + + EXPECT_EQ(util::JavaObjectToVariant(env_, array), expected) + << "Failed to convert Array of Object{Integer, Float, String, Null}"; + EXPECT_EQ(util::JavaObjectToVariant(env_, container), expected) + << "Failed to convert ArrayList{Integer, Float, String, Null}"; + env_->DeleteLocalRef(array); + env_->DeleteLocalRef(container); + } + // Map type converts to Variant::kTypeMap. + { + // Test a HashMap of String to {Integer, Float, String, Null} + // Only test keys that are strings, as that's all Java provides. + jobject container = env_->NewObject( + hash_map::GetClass(), hash_map::GetMethodId(hash_map::kConstructor)); + { + // Element 1: Integer + jobject key = env_->NewStringUTF("one"); + jobject obj = env_->NewObject( + integer_class::GetClass(), + integer_class::GetMethodId(integer_class::kConstructor), + static_cast(kTestValueInt)); + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + env_->DeleteLocalRef(obj); + } + { + // Element 2: Float + jobject key = env_->NewStringUTF("two"); + jobject obj = + env_->NewObject(float_class::GetClass(), + float_class::GetMethodId(float_class::kConstructor), + static_cast(kTestValueFloat)); + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + env_->DeleteLocalRef(obj); + } + { + // Element 3: String + jobject key = env_->NewStringUTF("three"); + jobject obj = env_->NewStringUTF(kTestValueString); + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + env_->DeleteLocalRef(obj); + } + { + // Element 4: Null + jobject key = env_->NewStringUTF("four"); + jobject obj = nullptr; + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + } + + Variant expected = Variant::EmptyMap(); + expected.map()[Variant("one")] = Variant::FromInt64(kTestValueInt); + expected.map()[Variant("two")] = Variant::FromDouble(kTestValueFloat); + expected.map()[Variant("three")] = + Variant::FromMutableString(kTestValueString); + expected.map()[Variant("four")] = Variant::Null(); + + EXPECT_EQ(util::JavaObjectToVariant(env_, container), expected) + << "Failed to convert Map of String to {Integer, Float, String, Null}"; + env_->DeleteLocalRef(container); + } + // TODO(b/113619056): Test complex containers containing other containers. +} + +// TODO(b/113619056): Tests for VariantToJavaObject. + +} // namespace util +} // namespace firebase diff --git a/app/tests/util_ios_test.mm b/app/tests/util_ios_test.mm new file mode 100644 index 0000000000..b9c231f8bc --- /dev/null +++ b/app/tests/util_ios_test.mm @@ -0,0 +1,650 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +#import + +#include "app/src/include/firebase/variant.h" +#include "app/src/util_ios.h" + +typedef firebase::util::ObjCPointer NSStringCpp; +OBJ_C_PTR_WRAPPER_NAMED(NSStringHandle, NSString); +OBJ_C_PTR_WRAPPER(NSString); + +@interface ObjCPointerTests : XCTestCase +@end + +@implementation ObjCPointerTests + +- (void)testConstructAndGet { + NSStringCpp cpp; + NSStringHandle handle; + NSStringPointer pointer; + XCTAssertEqual(cpp.get(), nil); + XCTAssertEqual(handle.get(), nil); + XCTAssertEqual(pointer.get(), nil); +} + +- (void)testConstructWithObjectAndGet { + NSString* nsstring = @"hello"; + NSStringCpp cpp(nsstring); + NSStringHandle handle(nsstring); + NSStringPointer pointer(nsstring); + NSStringHandle from_base_type(cpp); + XCTAssertEqual(cpp.get(), nsstring); + XCTAssertEqual(handle.get(), nsstring); + XCTAssertEqual(pointer.get(), nsstring); + XCTAssertEqual(from_base_type.get(), nsstring); +} + +- (void)testRelease { + NSString *nsstring = @"hello"; + NSStringCpp cpp(nsstring); + XCTAssertEqual(cpp.get(), nsstring); + cpp.release(); + XCTAssertEqual(cpp.get(), nil); +} + +- (void)testBoolOperator { + NSStringCpp cpp(@"hello"); + XCTAssertTrue(cpp); + cpp.release(); + XCTAssertFalse(cpp); +} + +- (void)testReset { + NSString* hello = @"hello"; + NSString* goodbye = @"goodbye"; + NSStringCpp cpp(hello); + XCTAssertEqual(cpp.get(), hello); + cpp.reset(goodbye); + XCTAssertEqual(cpp.get(), goodbye); +} + +- (void)testAssign { + NSString* hello = @"hello"; + NSString* goodbye = @"goodbye"; + NSStringCpp cpp(hello); + NSStringHandle handle(hello); + NSStringPointer pointer(hello); + XCTAssertEqual(cpp.get(), hello); + XCTAssertEqual(*cpp, hello); + XCTAssertEqual(handle.get(), hello); + XCTAssertEqual(*handle, hello); + XCTAssertEqual(pointer.get(), hello); + XCTAssertEqual(*pointer, hello); + XCTAssertEqual((*cpp).length, 5); + XCTAssertEqual((*handle).length, 5); + XCTAssertEqual((*pointer).length, 5); + cpp = goodbye; + handle = goodbye; + pointer = goodbye; + XCTAssertEqual(cpp.get(), goodbye); + XCTAssertEqual(handle.get(), goodbye); + XCTAssertEqual(pointer.get(), goodbye); +} + +- (void)testSafeGet { + NSString* hello = @"hello"; + NSStringCpp cpp(hello); + NSStringHandle handle(hello); + NSStringPointer pointer(hello); + XCTAssertEqual(NSStringCpp::SafeGet(&cpp), hello); + XCTAssertEqual(NSStringHandle::SafeGet(&handle), hello); + XCTAssertEqual(NSStringPointer::SafeGet(&pointer), hello); + cpp.release(); + handle.release(); + pointer.release(); + XCTAssertEqual(NSStringCpp::SafeGet(&cpp), nil); + XCTAssertEqual(NSStringHandle::SafeGet(&handle), nil); + XCTAssertEqual(NSStringPointer::SafeGet(&pointer), nil); + XCTAssertEqual(NSStringCpp::SafeGet(nullptr), nil); +} + +@end + +using ::firebase::Variant; +using ::firebase::util::IdToVariant; +using ::firebase::util::VariantToId; + +@interface IdToVariantTests : XCTestCase +@end + +@implementation IdToVariantTests + +- (void)testNil { + // Check that nil maps to a null variant and that a non-nil value does not map + // to a null variant. + { + // Nil id. + id value = nil; + Variant variant = IdToVariant(value); + XCTAssertTrue(variant.is_null()); + } + { + // Non-nil id. + id value = [NSNumber numberWithInteger:0]; + Variant variant = IdToVariant(value); + XCTAssertFalse(variant.is_null()); + } +} + +- (void)testInteger { + // Check that integers map to the correct variant, even when those numbers + // exceed the maximum integer value. + { + // Check that the integer 0 maps to an integer variant holding 0. + id number = [NSNumber numberWithInteger:0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 0); + } + { + // Check that the integer 1 maps to an integer variant holding 1. + id number = [NSNumber numberWithInteger:1]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 1); + } + { + // Check that the integer 10 maps to an integer variant holding 10. + id number = [NSNumber numberWithInteger:10]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 10); + } + { + // Check that a variant can hander an integer larger than the largest 32 bit + // int. + id number = [NSNumber numberWithInteger:5000000000ll]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 5000000000ll); + } +} + +- (void)testDouble { + // Check that integers map to the correct variant, even when those numbers + // exceed the maximum 64 bit integer value. + { + // Check that the double 0.0 maps to a double variant holding 0.0. + id number = [NSNumber numberWithDouble:0.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 0.0); + } + { + // Check that a variant can hander fractional values. + id number = [NSNumber numberWithDouble:0.5]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 0.5); + } + { + // Check that the double 1.0 maps to a double variant holding 1.0. + id number = [NSNumber numberWithDouble:1.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 1.0); + } + { + // Check that the double 10.0 maps to a double variant holding 10.0. + id number = [NSNumber numberWithDouble:10.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 10.0); + } + { + // Check that a variant can hander a double larger than the largest 32 bit + // int. + id number = [NSNumber numberWithDouble:5000000000.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 5000000000.0); + } + { + // Check that a variant can hander a double larger than the largest 64 bit + // int. + id number = [NSNumber numberWithDouble:20000000000000000000.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 20000000000000000000.0); + } +} + +- (void)testBool { + // Check that boolean values map to the correct boolean variant. + { + id value = [NSNumber numberWithBool:YES]; + Variant variant = IdToVariant(value); + XCTAssertTrue(variant.is_bool()); + XCTAssertTrue(variant.bool_value() == true); + } + { + id value = [NSNumber numberWithBool:NO]; + Variant variant = IdToVariant(value); + XCTAssertTrue(variant.is_bool()); + XCTAssertTrue(variant.bool_value() == false); + } +} + +- (void)testString { + // Check that NSStrings map to the correct std::string variants. + { + // Empty string. + id str = @""; + Variant variant = IdToVariant(str); + XCTAssertTrue(variant.is_string()); + XCTAssertTrue(variant.is_mutable_string()); + XCTAssertTrue(variant.string_value() == std::string("")); + } + { + // Non-empty string. + id str = @"Test With Very Very Long String"; + Variant variant = IdToVariant(str); + XCTAssertTrue(variant.is_string()); + XCTAssertTrue(variant.is_mutable_string()); + XCTAssertTrue(variant.string_value() == std::string("Test With Very Very Long String")); + } +} + +- (void)testVector { + // Check that NSArrays map to the correct vector variants. + { + // Empty NSArray to empty vector. + id array = @[]; + std::vector expected; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray of numbers to vector of integer variants. + id array = @[ @1, @2, @3, @4, @5 ]; + std::vector expected{1, 2, 3, 4, 5}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray of NSStrings to vector of std::string variants. + id array = @[ @"This", @"is", @"a", @"test." ]; + std::vector expected{"This", "is", "a", "test."}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray of various types to vector of variants holding varying types. + id array = @[ @"Different types", @10, @3.14 ]; + std::vector expected{std::string("Different types"), 10, 3.14}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray containing an NSArray and an NSDictionary to an std::vector + // holding an std::vector and std::map + id array = @[ @[ @1, @2, @3 ], @{ @4 : @5, @6 : @7, @8 : @9 } ]; + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::vector expected{Variant(vector_element), + Variant(map_element)}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } +} + +- (void)testMap { + { + // Check that an empty NSDictionary maps to an empty std::map. + id dictionary = @{}; + std::map expected; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } + { + // Check that a NSDictionary of strings to numbers maps to a std::map of + // string variants to number variants. + id dictionary = @{ + @"test1" : @1, + @"test2" : @2, + @"test3" : @3, + @"test4" : @4, + @"test5" : @5 + }; + std::map expected{ + std::make_pair(Variant("test1"), Variant(1)), + std::make_pair(Variant("test2"), Variant(2)), + std::make_pair(Variant("test3"), Variant(3)), + std::make_pair(Variant("test4"), Variant(4)), + std::make_pair(Variant("test5"), Variant(5))}; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } + { + // Check that a NSDictionary of various types maps to a std::map of variants + // holding various types. + id dictionary = @{ @20 : @"Different types", @6.28 : @10, @"Blah" : @3.14 }; + std::map expected{ + std::make_pair(Variant(20), Variant("Different types")), + std::make_pair(Variant(6.28), Variant(10)), + std::make_pair(Variant("Blah"), Variant(3.14))}; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } + { + // Check that a NSDictionary of NSArray-to-NSDictionary maps to an std::map + // of vector-to-map + id dictionary = @{ @[ @1, @2, @3 ] : @{@4 : @5, @6 : @7, @8 : @9} }; + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::map expected{ + std::make_pair(Variant(vector_element), Variant(map_element))}; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } +} + +@end + +@interface VariantToIdTests : XCTestCase +@end + +@implementation VariantToIdTests + +- (void)testNil { + // Check that null variant maps to nil variant and that a non-null does not + // map to a nil id. + { + // Null variant. + Variant variant; + id value = VariantToId(variant); + XCTAssertTrue(value == [NSNull null]); + } + { + // Non-null variant. + Variant variant(10); + id value = VariantToId(variant); + XCTAssertTrue(value != [NSNull null]); + } +} + +- (void)testInteger { + // Check that integers map to the correct variant, even when those numbers + // exceed the maximum integer value. + { + // Check that the variant 0 maps to an NSNumber holding 0. + Variant variant(0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 0); + } + { + // Check that the variant 1 maps to an NSNumber holding 1. + Variant variant(1); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 1); + } + { + // Check that the variant 10 maps to an NSNumber holding 10. + Variant variant(10); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 10); + } + { + // Check that a variant can hander an integer larger than the largest 32 bit + // int. + Variant variant(5000000000ll); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 5000000000ll); + } +} + +- (void)testDouble { + // Check that doubles map to the correct variant, even when those numbers + // exceed the maximum integer value. + { + // Check that the variant 0.0 maps to an NSNumber holding 0.0. + Variant variant(0.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 0.0); + } + { + // Check that a variant can hander fractional values. + Variant variant(0.5); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 0.5); + } + { + // Check that the variant 1.0 maps to an NSNumber holding 1.0. + Variant variant(1.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 1.0); + } + { + // Check that the variant 10.0 maps to an NSNumber holding 10.0. + Variant variant(10.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 10.0); + } + { + // Check that a variant can hander a double larger than the largest 32 bit + // int. + Variant variant(5000000000.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 5000000000.0); + } + { + // Check that a variant can hander a double larger than the largest 64 bit + // int. + Variant variant(20000000000000000000.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 20000000000000000000.0); + } +} + +- (void)testBool { + // Check that boolean variants map to the correct NSNumbers. + { + Variant variant(true); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSNumber class]]); + XCTAssertTrue([value boolValue] == YES); + } + { + Variant variant(false); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSNumber class]]); + XCTAssertTrue([value boolValue] == NO); + } +} + +- (void)testString { + { + // Empty static string. + const char* input_string = ""; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@""]); + } + { + // Empty mutable string. + std::string input_string = ""; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@""]); + } + { + // Non-empty static string. + const char* input_string = "Test"; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@"Test"]); + } + { + // Non-empty mutable string. + std::string input_string = "Test"; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@"Test"]); + } +} + +- (void)testVector { + // Check that std::vectors map to NSArrays, even when those numbers + // exceed the maximum integer value. + { + // Empty std::vector to empty NSArray. + std::vector vector; + id expected = @[]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector of integers to NSArray of NSNumbers. + std::vector vector{1, 2, 3, 4, 5}; + id expected = @[ @1, @2, @3, @4, @5 ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector of static and mutable strings to NSArray of NSStrings. + std::vector vector{"This", std::string("is"), "a", + std::string("test.")}; + id expected = @[ @"This", @"is", @"a", @"test." ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector of various types to NSArray of various types. + std::vector vector{"Different types", 10, 3.14}; + id expected = @[ @"Different types", @10, @3.14 ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector containing a vector and map to an NSArray containing an + // NSArray and an NSDictionary. + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::vector vector{Variant(vector_element), Variant(map_element)}; + id expected = @[ @[ @1, @2, @3 ], @{ @4 : @5, @6 : @7, @8 : @9 } ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } +} + +- (void)testMap { + // Check that an std::maps map to NSDictionarys with correct types. + { + // Check that empty std::map maps to an empty NSDictionary. + std::map map; + id expected = @{}; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } + { + // Check that an std::map of strings to numbers maps to an NSDictionary of + // NSString to NSNumbers. + std::map map{ + std::make_pair(Variant("test1"), Variant(1)), + std::make_pair(Variant("test2"), Variant(2)), + std::make_pair(Variant("test3"), Variant(3)), + std::make_pair(Variant("test4"), Variant(4)), + std::make_pair(Variant("test5"), Variant(5))}; + id expected = @{ + @"test1" : @1, + @"test2" : @2, + @"test3" : @3, + @"test4" : @4, + @"test5" : @5 + }; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } + { + // Check that an std::map of various types maps to an NSDictionary of + // various types. + std::map map{ + std::make_pair(Variant(20), Variant(std::string("Different types"))), + std::make_pair(Variant(6.28), Variant(10)), + std::make_pair(Variant("Blah"), Variant(3.14))}; + id expected = @{ @20 : @"Different types", @6.28 : @10, @"Blah" : @3.14 }; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } + { + // Check that an std::map of vector-to-map maps to a NSDictionary of + // NSArray-to-NSDictionary + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::map map{ + std::make_pair(Variant(vector_element), Variant(map_element))}; + id expected = @{ @[ @1, @2, @3 ] : @{@4 : @5, @6 : @7, @8 : @9} }; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } +} + +@end diff --git a/app/tests/uuid_test.cc b/app/tests/uuid_test.cc new file mode 100644 index 0000000000..fffe9f8568 --- /dev/null +++ b/app/tests/uuid_test.cc @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/uuid.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Contains; +using ::testing::Ne; + +// Generate a UUID and make sure it's not zero. +TEST(UuidTest, Generate) { + firebase::internal::Uuid uuid; + memset(&uuid, 0, sizeof(uuid)); + uuid.Generate(); + EXPECT_THAT(uuid.data, Contains(Ne(0))); +} + +// Generate two UUIDs and verify they're different. +TEST(UuidTest, GenerateDifferent) { + firebase::internal::Uuid uuid[2]; + memset(&uuid, 0, sizeof(uuid)); + uuid[0].Generate(); + uuid[1].Generate(); + EXPECT_THAT(memcmp(uuid[0].data, uuid[1].data, sizeof(uuid[0].data)), Ne(0)); +} diff --git a/app/tests/variant_test.cc b/app/tests/variant_test.cc new file mode 100644 index 0000000000..cde874e459 --- /dev/null +++ b/app/tests/variant_test.cc @@ -0,0 +1,1186 @@ +/* + * Copyright 2016 Google LLC + * + * 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 "app/src/include/firebase/variant.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Eq; +using ::testing::Gt; +using ::testing::IsEmpty; +using ::testing::Lt; +using ::testing::Ne; +using ::testing::Not; +using ::testing::Pair; +using ::testing::Property; +using ::testing::ResultOf; +using ::testing::StrEq; +using ::testing::UnorderedElementsAre; + +namespace firebase { +namespace internal { +class VariantInternal { + public: + static constexpr uint32_t kInternalTypeSmallString = + Variant::kInternalTypeSmallString; + + static uint32_t type(const Variant& v) { + return v.type_; + } +}; +} // namespace internal +} // namespace firebase + +using firebase::internal::VariantInternal; + +namespace firebase { +namespace testing { + +const int64_t kTestInt64 = 12345L; +const char* kTestString = "Hello, world!"; +const std::string kTestSmallString = " kTestVector = {int64_t(1L), "one", true, 1.0}; +// NOLINTNEXTLINE +const std::vector kTestComplexVector = {int64_t(2L), "two", + kTestVector, false, 2.0}; +const uint8_t kTestBlobData[] = {89, 0, 65, 198, 4, 99, 0, 9}; +const size_t kTestBlobSize = sizeof(kTestBlobData); // size in bytes +std::map g_test_map; // NOLINT +std::map g_test_complex_map; // NOLINT + +class VariantTest : public ::testing::Test { + protected: + VariantTest() {} + void SetUp() override { + g_test_map.clear(); + g_test_map["first"] = 101; + g_test_map["second"] = 202.2; + g_test_map["third"] = "three"; + + g_test_complex_map.clear(); + g_test_complex_map["one"] = kTestString; + g_test_complex_map[2] = 123; + g_test_complex_map[3.0] = + Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + g_test_complex_map[kTestVector] = kTestComplexVector; + g_test_complex_map[std::string("five")] = g_test_map; + g_test_complex_map[Variant::FromMutableBlob(kTestBlobData, kTestBlobSize)] = + kTestMutableString; + } +}; + +TEST_F(VariantTest, TestScalarTypes) { + { + Variant v; + EXPECT_THAT(v.type(), Eq(Variant::kTypeNull)); + EXPECT_TRUE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestInt64); + EXPECT_THAT(v.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v.int64_value(), Eq(kTestInt64)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + // Ensure that 0 comes through as an integer, not a bool. + Variant v(0); + EXPECT_THAT(v.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v.int64_value(), Eq(0)); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestString); + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v.string_value(), Eq(kTestString)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestSmallString); + EXPECT_THAT(VariantInternal::type(v), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v.string_value(), Eq(kTestSmallString)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + + // Should be able to upgrade to mutable string + EXPECT_THAT(v.mutable_string(), Eq(kTestSmallString)); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableString)); + } + { + Variant v(kTestMutableString); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v.mutable_string(), Eq(kTestMutableString)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestBool); + EXPECT_THAT(v.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v.bool_value(), Eq(kTestBool)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestDouble); + EXPECT_THAT(v.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v.double_value(), Eq(kTestDouble)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } +} + +TEST_F(VariantTest, TestInvalidTypeAsserts1) { + { + Variant v; + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestInt64); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestDouble); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestBool); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } +} + +TEST_F(VariantTest, TestInvalidTypeAsserts2) { + { + Variant v(kTestString); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestMutableString); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestVector); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + } + { + Variant v(g_test_map); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } +} + +TEST_F(VariantTest, TestMutableStringPromotion) { + Variant v("Hello!"); + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v.string_value(), StrEq("Hello!")); + (void)v.mutable_string(); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v.mutable_string(), StrEq("Hello!")); + EXPECT_THAT(v.string_value(), StrEq("Hello!")); + v.mutable_string()[5] = '?'; + EXPECT_THAT(v.mutable_string(), StrEq("Hello?")); + EXPECT_THAT(v.string_value(), StrEq("Hello?")); + v.set_string_value("Goodbye."); + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v.string_value(), StrEq("Goodbye.")); +} + +TEST_F(VariantTest, TestSmallString) { + std::string max_small_str; + + if (sizeof(void*) == 8) { + max_small_str = "1234567812345678"; // 16 bytes on x64 + } else { + max_small_str = "12345678"; // 8 bytes on x32 + } + + std::string small_str = max_small_str; + small_str.pop_back(); // Make room for the trailing \0. + + // Test construction from std::string + Variant v1(small_str); + EXPECT_THAT(VariantInternal::type(v1), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v1.string_value(), StrEq(small_str.c_str())); + + // Test copy constructor + Variant v1c(v1); + EXPECT_THAT(VariantInternal::type(v1c), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v1c.string_value(), StrEq(small_str.c_str())); + +#ifdef FIREBASE_USE_MOVE_OPERATORS + // Test move constructor + Variant temp(small_str); + Variant v2(std::move(temp)); + EXPECT_THAT(VariantInternal::type(v2), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v2.string_value(), StrEq(small_str.c_str())); +#endif + + // Test construction of string bigger than max + Variant v3(max_small_str); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v3.string_value(), StrEq(max_small_str.c_str())); + + // Copy normal string to ensure type changes to mutable string + v1 = v3; + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1.string_value(), StrEq(max_small_str.c_str())); + + // Test set using smaller string + v1c.set_mutable_string("a"); + EXPECT_THAT(VariantInternal::type(v1c), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v1c.string_value(), StrEq("a")); + + // Test can set small string as mutable + v1c.set_mutable_string("b", false); + EXPECT_THAT(v1c.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1c.string_value(), StrEq("b")); +} + +TEST_F(VariantTest, TestBasicVector) { + Variant v1(kTestInt64); + Variant v2(kTestString); + Variant v3(kTestDouble); + Variant v4(kTestBool); + Variant v5(kTestMutableString); + Variant v(std::vector{v1, v2, v3, v4, v5}); + + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_TRUE(v.is_container_type()); + EXPECT_FALSE(v.is_fundamental_type()); + EXPECT_THAT( + v.vector(), + ElementsAre( + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(kTestInt64))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, Eq(kTestString))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(kTestDouble))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeBool)), + Property(&Variant::bool_value, Eq(kTestBool))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeMutableString)), + Property(&Variant::mutable_string, Eq(kTestMutableString))))); +} + +TEST_F(VariantTest, TestConstructingVectorViaTemplate) { + { + std::vector list{8, 6, 7, 5, 3, 0, 9}; + Variant v(list); + EXPECT_THAT( + v.vector(), + ElementsAre(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(8))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(6))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(7))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(5))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(3))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(0))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(9))))); + } + { + std::vector list{0, 1.1, 2.2, 3.3, 4}; + Variant v(list); + EXPECT_THAT( + v.vector(), + ElementsAre(AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(0))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(1.1))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(2.2))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(3.3))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(4))))); + } + { + std::vector list1 { + "hello", + "world", + "how", + "are", + "you with more chars" + }; + std::vector list2 { + "hello", + "world", + "how", + "are", + "you with more chars" + }; + Variant v1(list1), v2(list2); + EXPECT_THAT( + v1.vector(), + ElementsAre( + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("hello"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("world"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("how"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("are"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, + StrEq("you with more chars"))))); + EXPECT_THAT( + v2.vector(), + ElementsAre( + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("hello"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("world"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("how"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("are"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(Variant::kTypeMutableString)), + Property(&Variant::string_value, + StrEq("you with more chars"))))); + + // Static and mutable strings are considered equal. So these should be + // equal. + EXPECT_EQ(v1, v2); + } +} + +TEST_F(VariantTest, TestNestedVectors) { + Variant v(std::vector{ + kTestInt64, std::vector{10, 20, 30, 40, 50}, + std::vector{"apples", "oranges", "lemons"}, + std::vector{"sneezy", "bashful", "dopey", "doc"}, + std::vector{true, false, false, true, false}, kTestString, + std::vector{3.14159, 2.71828, 1.41421, 0}, kTestBool, + std::vector{int64_t(100L), "one hundred", 100.0, + std::vector{}, Variant(), 0}, + kTestDouble}); + + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT( + v.vector(), + ElementsAre( + Property(&Variant::int64_value, Eq(kTestInt64)), + Property(&Variant::vector, + ElementsAre(Property(&Variant::int64_value, Eq(10)), + Property(&Variant::int64_value, Eq(20)), + Property(&Variant::int64_value, Eq(30)), + Property(&Variant::int64_value, Eq(40)), + Property(&Variant::int64_value, Eq(50)))), + Property( + &Variant::vector, + ElementsAre(Property(&Variant::string_value, StrEq("apples")), + Property(&Variant::string_value, StrEq("oranges")), + Property(&Variant::string_value, StrEq("lemons")))), + Property( + &Variant::vector, + ElementsAre(Property(&Variant::string_value, StrEq("sneezy")), + Property(&Variant::string_value, StrEq("bashful")), + Property(&Variant::string_value, StrEq("dopey")), + Property(&Variant::string_value, StrEq("doc")))), + Property(&Variant::vector, + ElementsAre(Property(&Variant::bool_value, Eq(true)), + Property(&Variant::bool_value, Eq(false)), + Property(&Variant::bool_value, Eq(false)), + Property(&Variant::bool_value, Eq(true)), + Property(&Variant::bool_value, Eq(false)))), + Property(&Variant::string_value, Eq(kTestString)), + Property(&Variant::vector, + ElementsAre(Property(&Variant::double_value, Eq(3.14159)), + Property(&Variant::double_value, Eq(2.71828)), + Property(&Variant::double_value, Eq(1.41421)), + Property(&Variant::double_value, Eq(0)))), + Property(&Variant::bool_value, Eq(kTestBool)), + Property(&Variant::vector, + ElementsAre( + Property(&Variant::int64_value, Eq(100L)), + Property(&Variant::string_value, StrEq("one hundred")), + Property(&Variant::double_value, Eq(100.0)), + Property(&Variant::vector, IsEmpty()), + Property(&Variant::is_null, Eq(true)), + Property(&Variant::int64_value, Eq(0)))), + Property(&Variant::double_value, Eq(kTestDouble)))); +} + +TEST_F(VariantTest, TestBasicMap) { + { + // Map of strings to Variant. + std::map m; + m["hello"] = kTestInt64; + m["world"] = kTestString; + m["how"] = kTestDouble; + m["are"] = kTestBool; + m["you"] = Variant(); + m["dude"] = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + Variant v(m); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_TRUE(v.is_container_type()); + EXPECT_FALSE(v.is_fundamental_type()); + EXPECT_THAT(v.map(), + UnorderedElementsAre( + Pair(Property(&Variant::string_value, StrEq("hello")), + Property(&Variant::int64_value, Eq(kTestInt64))), + Pair(Property(&Variant::string_value, StrEq("world")), + Property(&Variant::string_value, Eq(kTestString))), + Pair(Property(&Variant::string_value, StrEq("how")), + Property(&Variant::double_value, Eq(kTestDouble))), + Pair(Property(&Variant::string_value, StrEq("are")), + Property(&Variant::bool_value, Eq(kTestBool))), + Pair(Property(&Variant::string_value, StrEq("you")), + Property(&Variant::is_null, Eq(true))), + Pair(Property(&Variant::string_value, StrEq("dude")), + Property(&Variant::blob_size, Eq(kTestBlobSize))))); + } + { + std::map m; + m["0"] = kTestInt64; + m[0] = kTestString; + m[0.0] = kTestBool; + m[false] = kTestDouble; + m[Variant::Null()] = kTestMutableString; + Variant v(m); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT( + v.map(), + UnorderedElementsAre( + Pair(AllOf(Property(&Variant::is_string, Eq(true)), + Property(&Variant::string_value, StrEq("0"))), + AllOf(Property(&Variant::is_int64, Eq(true)), + Property(&Variant::int64_value, Eq(kTestInt64)))), + Pair(AllOf(Property(&Variant::is_int64, Eq(true)), + Property(&Variant::int64_value, Eq(0))), + AllOf(Property(&Variant::is_string, Eq(true)), + Property(&Variant::string_value, Eq(kTestString)))), + Pair(AllOf(Property(&Variant::is_double, Eq(true)), + Property(&Variant::double_value, Eq(0.0))), + AllOf(Property(&Variant::is_bool, Eq(true)), + Property(&Variant::bool_value, Eq(kTestBool)))), + Pair(AllOf(Property(&Variant::is_bool, Eq(true)), + Property(&Variant::bool_value, Eq(false))), + AllOf(Property(&Variant::is_double, Eq(true)), + Property(&Variant::double_value, Eq(kTestDouble)))), + Pair(Property(&Variant::is_null, Eq(true)), + AllOf(Property(&Variant::is_string, Eq(true)), + Property(&Variant::mutable_string, + Eq(kTestMutableString)))))); + } + { + // Ensure that if you reassign to a key in the map, it modifies it. + std::vector vect1 = {1, 2, 3, 4}; + std::vector vect2 = {1, 2, 4, 4}; + std::vector vect1copy = {1, 2, 3, 4}; + Variant v = Variant::EmptyMap(); + v.map()[vect1] = "Hello"; + v.map()[vect2] = "world"; + EXPECT_THAT(v.map(), + UnorderedElementsAre( + Pair(Property(&Variant::vector, ElementsAre(1, 2, 3, 4)), + Property(&Variant::string_value, StrEq("Hello"))), + Pair(Property(&Variant::vector, ElementsAre(1, 2, 4, 4)), + Property(&Variant::string_value, StrEq("world"))))); + EXPECT_THAT(vect1, Eq(vect1copy)); + v.map()[vect1copy] = "Goodbye"; + EXPECT_THAT(v.map(), + UnorderedElementsAre( + Pair(Property(&Variant::vector, ElementsAre(1, 2, 3, 4)), + Property(&Variant::string_value, StrEq("Goodbye"))), + Pair(Property(&Variant::vector, ElementsAre(1, 2, 4, 4)), + Property(&Variant::string_value, StrEq("world"))))); + } +} + +TEST_F(VariantTest, TestConstructingMapViaTemplate) { + { + std::map m{std::make_pair(23, "apple"), + std::make_pair(45, "banana"), + std::make_pair(67, "orange")}; + Variant v(m); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT( + v.map(), + UnorderedElementsAre( + Pair(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(23))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("apple")))), + Pair(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(45))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("banana")))), + Pair(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(67))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("orange")))))); + } +} + +TEST_F(VariantTest, TestNestedMaps) { + // TODO(jsimantov): Implement tests for maps of maps. +} + +TEST_F(VariantTest, TestComplexNesting) { + // TODO(jsimantov): Implement tests for complex nesting, e.g. maps of vectors + // of maps of etc. +} + +TEST_F(VariantTest, TestCopyAndAssignment) { + // Test copy constructor and assignment operator. + { + Variant v1(kTestString); + Variant v2(kTestInt64); + Variant v3(kTestMutableString); + Variant v4(kTestVector); + + EXPECT_THAT(v1.string_value(), Eq(kTestString)); + EXPECT_THAT(v2.int64_value(), Eq(kTestInt64)); + EXPECT_THAT(v3.mutable_string(), Eq(kTestMutableString)); + + v1 = v2; + EXPECT_THAT(v1.int64_value(), Eq(kTestInt64)); + EXPECT_THAT(v2.int64_value(), Eq(kTestInt64)); + + v1 = v3; + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1.mutable_string(), Eq(kTestMutableString)); + EXPECT_THAT(v3.mutable_string(), Eq(kTestMutableString)); + // Ensure they don't point to the same mutable string. + EXPECT_THAT(&v1.mutable_string(), Ne(&v3.mutable_string())); + + v1 = v4; + EXPECT_THAT(v1.vector(), Eq(kTestVector)); + EXPECT_THAT(v4.vector(), Eq(kTestVector)); + + Variant v5(kTestDouble); + Variant v6(v5); // NOLINT + EXPECT_THAT(v6, Eq(v5)); + + Variant v7(std::string("Mutable Longer string")); + Variant v8("Static"); + Variant v9(v7); + Variant v10(v8); // NOLINT + EXPECT_THAT(v7.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Mutable Longer string")); + v7 = v8; + EXPECT_THAT(v7.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Static")); + v7 = v9; + EXPECT_THAT(v7.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Mutable Longer string")); + v7 = v10; + EXPECT_THAT(v7.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Static")); + } + + // Test move constructor. + { + Variant v1(kTestMutableString); + EXPECT_THAT(v1.mutable_string(), Eq(kTestMutableString)); + const std::string* v1_ptr = &v1.mutable_string(); + + Variant v2(std::move(v1)); + // Ensure v2 has the value that v1 had. + EXPECT_THAT(v2.mutable_string(), Eq(kTestMutableString)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::string* v2_ptr = &v2.mutable_string(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + + Variant v3(kTestVector); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeVector)); + v3 = std::move(v2); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v3.mutable_string(), Eq(kTestMutableString)); + EXPECT_TRUE(v2.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::string* v3_ptr = &v3.mutable_string(); + EXPECT_THAT(v2_ptr, Eq(v3_ptr)); + } + + { + Variant v = std::string("Hello"); + EXPECT_THAT(v, Eq("Hello")); + v = *&v; + EXPECT_THAT(v, Eq("Hello")); + Variant v1 = std::move(v); + v = std::move(v1); + EXPECT_THAT(v, Eq("Hello")); + } + + { + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + Variant v2 = Variant::FromMutableBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1, Eq(v2)); + Variant v3 = v1; + EXPECT_THAT(v1, Eq(v2)); + EXPECT_THAT(v1, Eq(v3)); + EXPECT_THAT(v2, Eq(v3)); + v3 = v2; + EXPECT_THAT(v1, Eq(v2)); + EXPECT_THAT(v1, Eq(v3)); + EXPECT_THAT(v2, Eq(v3)); + Variant v0 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + v3 = std::move(v1); + EXPECT_THAT(v3, Eq(v0)); + v3 = std::move(v2); + EXPECT_THAT(v3, Eq(v0)); + } +} + +TEST_F(VariantTest, TestEqualityOperators) { + { + Variant v0(3); + Variant v1(3); + Variant v2(4); + EXPECT_EQ(v0, v1); + EXPECT_NE(v1, v2); + EXPECT_NE(v0, v2); + EXPECT_TRUE(v0 < v2 || v2 < v0); + EXPECT_FALSE(v0 < v2 && v2 < v0); + + EXPECT_THAT(v0, Not(Lt(v1))); + EXPECT_THAT(v0, Not(Gt(v1))); + } + { + Variant v1("Hello, world!"); + Variant v2(std::string("Hello, world!")); + EXPECT_EQ(v1, v2); + } + { + Variant v1(std::vector{0, 1}); + Variant v2(std::vector{1, 0}); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v2.type(), Eq(Variant::kTypeVector)); + EXPECT_FALSE(v1 < v2 && v2 < v1); + } +} + +TEST_F(VariantTest, TestDefaults) { + EXPECT_THAT(Variant::Null(), + Property(&Variant::type, Eq(Variant::kTypeNull))); + EXPECT_THAT(Variant::Zero(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(0)))); + EXPECT_THAT(Variant::ZeroPointZero(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(0.0)))); + EXPECT_THAT(Variant::False(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeBool)), + Property(&Variant::bool_value, Eq(false)))); + EXPECT_THAT(Variant::True(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeBool)), + Property(&Variant::bool_value, Eq(true)))); + EXPECT_THAT(Variant::EmptyString(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("")))); + EXPECT_THAT(Variant::EmptyMutableString(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeMutableString)), + Property(&Variant::string_value, StrEq("")))); + EXPECT_THAT(Variant::EmptyVector(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeVector)), + Property(&Variant::vector, IsEmpty()))); + EXPECT_THAT(Variant::EmptyMap(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeMap)), + Property(&Variant::map, IsEmpty()))); +} + +TEST_F(VariantTest, TestSettersAndGetters) { + // TODO(jsimantov): Implement tests for setters and getters, including + // modifying the contents of Variant containers. Also verifies that const + // getters work, and are returning the same thing as non-const versions. + { + Variant v; + const Variant& vconst = v; + EXPECT_THAT(v.type(), Eq(Variant::kTypeNull)); + v.set_int64_value(123); + EXPECT_THAT(v.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v.int64_value(), Eq(123)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(vconst.int64_value(), Eq(123)); + EXPECT_EQ(v, vconst); + v.set_vector({4, 5, 6}); + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v.vector(), ElementsAre(4, 5, 6)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(vconst.vector(), ElementsAre(4, 5, 6)); + EXPECT_EQ(v, vconst); + v.set_double_value(456.7); + EXPECT_THAT(v.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v.double_value(), Eq(456.7)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(vconst.double_value(), Eq(456.7)); + EXPECT_EQ(v, vconst); + v.set_bool_value(false); + EXPECT_THAT(v.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v.bool_value(), Eq(false)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(vconst.bool_value(), Eq(false)); + EXPECT_EQ(v, vconst); + v.set_map({std::make_pair(33, 44), std::make_pair(55, 66)}); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v.map(), UnorderedElementsAre(Pair(33, 44), Pair(55, 66))); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(vconst.map(), UnorderedElementsAre(Pair(33, 44), Pair(55, 66))); + EXPECT_EQ(v, vconst); + } +} + +TEST_F(VariantTest, TestConversionFunctions) { + { + EXPECT_EQ(Variant::Null().AsBool(), Variant::False()); + EXPECT_EQ(Variant::Zero().AsBool(), Variant::False()); + EXPECT_EQ(Variant::ZeroPointZero().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyMap().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyVector().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyString().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyMutableString().AsBool(), Variant::False()); + + EXPECT_EQ(Variant::One().AsBool(), Variant::True()); + EXPECT_EQ(Variant::OnePointZero().AsBool(), Variant::True()); + EXPECT_EQ(Variant(123).AsBool(), Variant::True()); + EXPECT_EQ(Variant(456.7).AsBool(), Variant::True()); + EXPECT_EQ(Variant("Hello").AsBool(), Variant::True()); + EXPECT_EQ(Variant::MutableStringFromStaticString("Hello").AsBool(), + Variant::True()); + EXPECT_EQ(Variant(std::vector{0}).AsBool(), Variant::True()); + EXPECT_EQ(Variant(std::map{std::make_pair(23, "apple"), + std::make_pair(45, "banana"), + std::make_pair(67, "orange")}) + .AsBool(), + Variant::True()); + EXPECT_EQ(Variant::FromStaticBlob(kTestBlobData, 0).AsBool(), + Variant::False()); + EXPECT_EQ(Variant::FromMutableBlob(kTestBlobData, 0).AsBool(), + Variant::False()); + EXPECT_EQ(Variant::FromStaticBlob(kTestBlobData, kTestBlobSize).AsBool(), + Variant::True()); + EXPECT_EQ(Variant::FromMutableBlob(kTestBlobData, kTestBlobSize).AsBool(), + Variant::True()); + } + { + const Variant vint(12345); + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + + Variant vdouble = vint.AsDouble(); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(vdouble.double_value(), Eq(12345.0)); + + const Variant vstring("87755.899"); + EXPECT_TRUE(vstring.is_string()); + vdouble = vstring.AsDouble(); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(vdouble.double_value(), Eq(87755.899)); + + EXPECT_EQ(vdouble.AsDouble(), vdouble); + + EXPECT_THAT(Variant::True().AsDouble(), Eq(Variant(1.0))); + EXPECT_THAT(Variant::False().AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant::False().AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant::Null().AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant(kTestVector).AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant(g_test_map).AsDouble(), Eq(Variant::ZeroPointZero())); + } + { + Variant vstring(std::string("38294")); + EXPECT_TRUE(vstring.is_string()); + + Variant vint = vstring.AsInt64(); + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(vint.int64_value(), Eq(38294)); + + // Check truncation. + Variant vdouble(399.9); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + vint = vdouble.AsInt64(); + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(vint.int64_value(), Eq(399)); + + EXPECT_THAT(Variant::True().AsInt64(), Eq(Variant(1))); + EXPECT_THAT(Variant::False().AsInt64(), Eq(Variant::Zero())); + EXPECT_THAT(Variant::Null().AsInt64(), Eq(Variant::Zero())); + EXPECT_THAT(Variant(kTestVector).AsInt64(), Eq(Variant::Zero())); + EXPECT_THAT(Variant(g_test_map).AsInt64(), Eq(Variant::Zero())); + } + { + Variant vint(int64_t(9223372036800000000L)); // almost max value + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + + Variant vstring = vint.AsString(); + EXPECT_TRUE(vstring.is_string()); + EXPECT_THAT(vstring.string_value(), StrEq("9223372036800000000")); + + Variant vdouble(34491282.2909820005297661); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + vstring = vdouble.AsString(); + EXPECT_TRUE(vstring.is_string()); + EXPECT_THAT(vstring.string_value(), StrEq("34491282.2909820005297661")); + + EXPECT_THAT(Variant::True().AsString(), Eq(Variant("true"))); + EXPECT_THAT(Variant::False().AsString(), Eq(Variant("false"))); + EXPECT_THAT(Variant::Null().AsString(), Eq(Variant::EmptyString())); + EXPECT_THAT(Variant(kTestVector).AsString(), Eq(Variant::EmptyString())); + EXPECT_THAT(Variant(g_test_map).AsString(), Eq(Variant::EmptyString())); + } +} + +// Copy a buffer+size into a vector, so gMock matchers can properly access it. +template +static std::vector AsVector(const T* buffer, size_t size_bytes) { + return std::vector(buffer, buffer + (size_bytes / sizeof(*buffer))); +} + +TEST_F(VariantTest, TestBlobs) { + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(v1.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(v1.blob_data(), Eq(kTestBlobData)); + + Variant v2 = Variant::FromMutableBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(v2.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(v2.blob_data(), Ne(kTestBlobData)); + + EXPECT_THAT(v1, Eq(v2)); + EXPECT_THAT(v1, Not(Lt(v2))); + EXPECT_THAT(v1, Not(Gt(v2))); + + // Make a copy of the mutable buffer that we can modify. + Variant v3 = v2; + + // Modify something within the mutable buffer, then ensure that they are + // no longer equal. Note that we don't care which is < the other. + reinterpret_cast(v3.mutable_blob_data())[kTestBlobSize / 2]++; + EXPECT_THAT(v1, Not(Eq(v3))); + EXPECT_THAT(v1, AnyOf(Lt(v3), Gt(v3))); + EXPECT_THAT(v2, Not(Eq(v3))); + EXPECT_THAT(v2, AnyOf(Lt(v3), Gt(v3))); + + // Ensure two blobs that are mostly the same but different sizes compare as + // different. + Variant v4 = Variant::FromMutableBlob(v2.blob_data(), v2.blob_size() - 1); + EXPECT_THAT(v2, Not(Eq(v4))); + EXPECT_THAT(v2, AnyOf(Lt(v4), Gt(v4))); + + // Check that two static blobs from the same data point to the same copy. + Variant v5 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v5.blob_data(), Eq(v1.blob_data())); + EXPECT_THAT(v5.blob_data(), Not(Eq(v2.blob_data()))); +} + +TEST_F(VariantTest, TestMutableBlobPromotion) { + Variant v = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(v.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + (void)v.mutable_blob_data(); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(v.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + // Modify one byte of the buffer. + reinterpret_cast(v.mutable_blob_data())[kTestBlobSize / 3] += 99; + uint8_t compare_buffer[kTestBlobSize]; + memcpy(compare_buffer, kTestBlobData, kTestBlobSize); + // Make the same change to a local buffer for comparison. + compare_buffer[kTestBlobSize / 3] += 99; + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(compare_buffer, kTestBlobSize)); + v.set_static_blob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + + // Check that two static blobs from the same data point to the same copy, but + // not after promotion. + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + Variant v2 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.blob_data(), Eq(v2.blob_data())); + (void)v2.mutable_blob_data(); + EXPECT_THAT(v1.blob_data(), Ne(v2.blob_data())); + + // Check that you can call set_mutable_blob on a Variant's own blob_data and + // blob_size. + Variant v3 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(AsVector(v3.blob_data(), v3.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + v3.set_mutable_blob(v3.blob_data(), v3.blob_size()); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(AsVector(v3.blob_data(), v3.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); +} + +TEST_F(VariantTest, TestMoveConstructorOnAllTypes) { + // Test fundamental/statically allocated types. + { + Variant v1(kTestInt64); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v1.int64_value(), Eq(kTestInt64)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v2.int64_value(), Eq(kTestInt64)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + Variant v1(kTestDouble); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v1.double_value(), Eq(kTestDouble)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v2.double_value(), Eq(kTestDouble)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + Variant v1(kTestBool); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v1.bool_value(), Eq(kTestBool)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v2.bool_value(), Eq(kTestBool)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + // Static string. + Variant v1(kTestString); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v1.string_value(), Eq(kTestString)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v2.string_value(), Eq(kTestString)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + // Static blob. + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(AsVector(v1.blob_data(), v1.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(AsVector(v2.blob_data(), v2.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + + // Test allocated types (mutable string, blob, containers) + { + Variant v1(kTestMutableString); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1.mutable_string(), Eq(kTestMutableString)); + const std::string* v1_ptr = &v1.mutable_string(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v2.mutable_string(), Eq(kTestMutableString)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::string* v2_ptr = &v2.mutable_string(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + { + Variant v1(kTestVector); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v1.vector(), Eq(kTestVector)); + const std::vector* v1_ptr = &v1.vector(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v2.vector(), Eq(kTestVector)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::vector* v2_ptr = &v2.vector(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + { + Variant v1(g_test_map); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v1.map(), Eq(g_test_map)); + const std::map* v1_ptr = &v1.map(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v2.map(), Eq(g_test_map)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::map* v2_ptr = &v2.map(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + { + Variant v1 = Variant::FromMutableBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(AsVector(v1.blob_data(), v1.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + const void* v1_ptr = v1.blob_data(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(AsVector(v2.blob_data(), v2.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const void* v2_ptr = v2.blob_data(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + // Test complex nested container type. + { + Variant v1(g_test_complex_map); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v1.map(), Eq(g_test_complex_map)); + const std::map* v1_ptr = &v1.map(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v2.map(), Eq(g_test_complex_map)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::map* v2_ptr = &v2.map(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + + // Test moving over existing variant values. + { + Variant v2(kTestString); + Variant v1 = Variant::Null(); + v2 = std::move(v1); + EXPECT_TRUE(v1.is_null()); // NOLINT + EXPECT_TRUE(v2.is_null()); + } + { + Variant v2(g_test_complex_map); + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v2.map(), Eq(g_test_complex_map)); + Variant v1 = kTestComplexVector; + EXPECT_TRUE(v1.is_vector()); + EXPECT_THAT(v1.vector(), Eq(kTestComplexVector)); + v2 = std::move(v1); + EXPECT_TRUE(v1.is_null()); // NOLINT + EXPECT_TRUE(v2.is_vector()); + EXPECT_THAT(v2.vector(), Eq(kTestComplexVector)); + } + { + Variant v(kTestComplexVector); + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v.vector(), Eq(kTestComplexVector)); + Variant v2(g_test_complex_map); + v.vector()[2] = std::move(v2); + EXPECT_THAT(v.vector()[2].type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v.vector()[2], Eq(g_test_complex_map)); + } +} + +} // namespace testing +} // namespace firebase diff --git a/app/tests/variant_util_test.cc b/app/tests/variant_util_test.cc new file mode 100644 index 0000000000..2c3e9196c0 --- /dev/null +++ b/app/tests/variant_util_test.cc @@ -0,0 +1,549 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/variant_util.h" + +#include +#include + +#include "app/src/include/firebase/variant.h" +#include "app/tests/flexbuffer_matcher.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/json_util.h" + +namespace { + +using ::firebase::Variant; +using ::firebase::testing::cppsdk::EqualsJson; +using ::firebase::util::JsonToVariant; +using ::firebase::util::VariantToFlexbuffer; +using ::firebase::util::VariantToJson; +using ::flexbuffers::GetRoot; +using ::testing::Eq; +using ::testing::Not; +using ::testing::StrEq; + +TEST(UtilDesktopTest, JsonToVariantNull) { + EXPECT_THAT(JsonToVariant("null"), Eq(Variant::Null())); +} + +TEST(UtilDesktopTest, JsonToVariantInt64) { + EXPECT_THAT(JsonToVariant("0"), Eq(Variant(0))); + EXPECT_THAT(JsonToVariant("100"), Eq(Variant(100))); + EXPECT_THAT(JsonToVariant("8000000000"), Eq(Variant(int64_t(8000000000L)))); + EXPECT_THAT(JsonToVariant("-100"), Eq(Variant(-100))); + EXPECT_THAT(JsonToVariant("-8000000000"), Eq(Variant(int64_t(-8000000000L)))); +} + +TEST(UtilDesktopTest, JsonToVariantDouble) { + EXPECT_THAT(JsonToVariant("0.0"), Eq(Variant(0.0))); + EXPECT_THAT(JsonToVariant("100.0"), Eq(Variant(100.0))); + EXPECT_THAT(JsonToVariant("8000000000.0"), Eq(Variant(8000000000.0))); + EXPECT_THAT(JsonToVariant("-100.0"), Eq(Variant(-100.0))); + EXPECT_THAT(JsonToVariant("-8000000000.0"), Eq(Variant(-8000000000.0))); +} + +TEST(UtilDesktopTest, JsonToVariantBool) { + EXPECT_THAT(JsonToVariant("true"), Eq(Variant::True())); + EXPECT_THAT(JsonToVariant("false"), Eq(Variant::False())); +} + +TEST(UtilDesktopTest, JsonToVariantString) { + EXPECT_THAT(JsonToVariant("\"Hello, World!\""), Eq(Variant("Hello, World!"))); + EXPECT_THAT(JsonToVariant("\"100\""), Eq(Variant("100"))); + EXPECT_THAT(JsonToVariant("\"false\""), Eq(Variant("false"))); +} + +TEST(UtilDesktopTest, JsonToVariantVector) { + EXPECT_THAT(JsonToVariant("[]"), Eq(Variant::EmptyVector())); + std::vector int_vector{1, 2, 3, 4}; + EXPECT_THAT(JsonToVariant("[1, 2, 3, 4]"), Eq(Variant(int_vector))); + std::vector mixed_vector{1, true, 3.5, "hello"}; + EXPECT_THAT(JsonToVariant("[1, true, 3.5, \"hello\"]"), Eq(mixed_vector)); + std::vector nested_vector{1, true, 3.5, "hello", int_vector}; + EXPECT_THAT(JsonToVariant("[1, true, 3.5, \"hello\", [1, 2, 3, 4]]"), + Eq(nested_vector)); +} + +TEST(UtilDesktopTest, JsonToVariantMap) { + EXPECT_THAT(JsonToVariant("{}"), Eq(Variant::EmptyMap())); + std::map int_map{ + std::make_pair("one_hundred", 100), + std::make_pair("two_hundred", 200), + std::make_pair("three_hundred", 300), + std::make_pair("four_hundred", 400), + }; + EXPECT_THAT(JsonToVariant("{" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + "}"), + Eq(Variant(int_map))); + std::map mixed_map{ + std::make_pair("boolean_value", true), + std::make_pair("int_value", 100), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + }; + EXPECT_THAT(JsonToVariant("{" + " \"boolean_value\": true," + " \"int_value\": 100," + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + "}"), + Eq(mixed_map)); + std::map nested_map{ + std::make_pair("int_map", int_map), + std::make_pair("mixed_map", mixed_map), + }; + EXPECT_THAT(JsonToVariant("{" + " \"int_map\": {" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + " }," + " \"mixed_map\": {" + " \"int_value\": 100," + " \"boolean_value\": true, " + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + " }" + "}"), + Eq(nested_map)); +} + +TEST(UtilDesktopTest, VariantToJsonNull) { + EXPECT_THAT(VariantToJson(Variant::Null()), EqualsJson("null")); +} + +TEST(UtilDesktopTest, VariantToJsonInt64) { + EXPECT_THAT(VariantToJson(Variant(0)), EqualsJson("0")); + EXPECT_THAT(VariantToJson(Variant(100)), EqualsJson("100")); + EXPECT_THAT(VariantToJson(Variant(int64_t(8000000000L))), + EqualsJson("8000000000")); + EXPECT_THAT(VariantToJson(Variant(-100)), EqualsJson("-100")); + EXPECT_THAT(VariantToJson(Variant(int64_t(-8000000000L))), + EqualsJson("-8000000000")); +} + +TEST(UtilDesktopTest, VariantToJsonDouble) { + EXPECT_THAT(VariantToJson(Variant(0.0)), EqualsJson("0")); + EXPECT_THAT(VariantToJson(Variant(100.0)), EqualsJson("100")); + EXPECT_THAT(VariantToJson(Variant(-100.0)), EqualsJson("-100")); +} + +TEST(UtilDesktopTest, VariantToJsonBool) { + EXPECT_THAT(VariantToJson(Variant::True()), EqualsJson("true")); + EXPECT_THAT(VariantToJson(Variant::False()), EqualsJson("false")); +} + +TEST(UtilDesktopTest, VariantToJsonStaticString) { + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello, World!")), + EqualsJson("\"Hello, World!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("100")), + EqualsJson("\"100\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("false")), + EqualsJson("\"false\"")); +} + +TEST(UtilDesktopTest, VariantToJsonMutableString) { + EXPECT_THAT(VariantToJson(Variant::FromMutableString("Hello, World!")), + EqualsJson("\"Hello, World!\"")); + EXPECT_THAT(VariantToJson(Variant::FromMutableString("100")), + EqualsJson("\"100\"")); + EXPECT_THAT(VariantToJson(Variant::FromMutableString("false")), + EqualsJson("\"false\"")); +} + +TEST(UtilDesktopTest, VariantToJsonWithEscapeCharacters) { + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello, \"World\"!")), + EqualsJson("\"Hello, \\\"World\\\"!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello, \\backslash\\!")), + EqualsJson("\"Hello, \\\\backslash\\\\!\"")); + EXPECT_THAT( + VariantToJson(Variant::FromStaticString("Hello, // forwardslash!")), + EqualsJson("\"Hello, \\/\\/ forwardslash!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello!\nHello again!")), + EqualsJson("\"Hello!\\nHello again!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("こんにちは")), + EqualsJson("\"\\u3053\\u3093\\u306B\\u3061\\u306F\"")); +} + +TEST(UtilDesktopTest, VariantToJsonVector) { + EXPECT_THAT(VariantToJson(Variant::EmptyVector()), EqualsJson("[]")); + EXPECT_THAT(VariantToJson(Variant::EmptyVector(), true), EqualsJson("[]")); + + std::vector int_vector{1, 2, 3, 4}; + EXPECT_THAT(VariantToJson(Variant(int_vector)), StrEq("[1,2,3,4]")); + EXPECT_THAT(VariantToJson(Variant(int_vector), true), + StrEq("[\n 1,\n 2,\n 3,\n 4\n]")); + + std::vector mixed_vector{1, true, 3.5, "hello"}; + EXPECT_THAT(VariantToJson(Variant(mixed_vector)), + StrEq("[1,true,3.5,\"hello\"]")); + EXPECT_THAT(VariantToJson(Variant(mixed_vector), true), + StrEq("[\n 1,\n true,\n 3.5,\n \"hello\"\n]")); + + std::vector nested_vector{1, true, 3.5, "hello", int_vector}; + EXPECT_THAT(VariantToJson(nested_vector), + StrEq("[1,true,3.5,\"hello\",[1,2,3,4]]")); + EXPECT_THAT(VariantToJson(nested_vector, true), + StrEq("[\n 1,\n true,\n 3.5,\n \"hello\",\n" + " [\n 1,\n 2,\n 3,\n 4\n ]\n]")); +} + +TEST(UtilDesktopTest, VariantToJsonMapWithStringKeys) { + EXPECT_THAT(VariantToJson(Variant::EmptyMap()), EqualsJson("{}")); + std::map int_map{ + std::make_pair("one_hundred", 100), + std::make_pair("two_hundred", 200), + std::make_pair("three_hundred", 300), + std::make_pair("four_hundred", 400), + }; + EXPECT_THAT(VariantToJson(Variant(int_map)), + EqualsJson("{" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + "}")); + std::map mixed_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + }; + EXPECT_THAT(VariantToJson(mixed_map), + EqualsJson("{" + " \"int_value\": 100," + " \"boolean_value\": true," + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + "}")); + std::map nested_map{ + std::make_pair("int_map", int_map), + std::make_pair("mixed_map", mixed_map), + }; + EXPECT_THAT(VariantToJson(nested_map), + EqualsJson("{" + " \"int_map\":{" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + " }," + " \"mixed_map\":{" + " \"int_value\": 100," + " \"boolean_value\": true," + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + " }" + "}")); + + // Test pretty printing with one key per map, since key order may vary. + std::map nested_one_key_map{ + std::make_pair("a", + std::vector{ + 1, 2, + std::map{ + std::make_pair("b", std::vector{3, 4}), + }}), + }; + EXPECT_THAT(VariantToJson(Variant(nested_one_key_map), true), + StrEq("{\n" + " \"a\": [\n" + " 1,\n" + " 2,\n" + " {\n" + " \"b\": [\n" + " 3,\n" + " 4\n" + " ]\n" + " }\n" + " ]\n" + "}")); +} + +TEST(UtilDesktopTest, VariantToJsonMapLegalNonStringKeys) { + // VariantToJson will convert fundamental types to strings. + std::map int_key_map{ + std::make_pair(100, "one_hundred"), + std::make_pair(200, "two_hundred"), + std::make_pair(300, "three_hundred"), + std::make_pair(400, "four_hundred"), + }; + EXPECT_THAT(VariantToJson(Variant(int_key_map)), + EqualsJson("{" + " \"100\": \"one_hundred\"," + " \"200\": \"two_hundred\"," + " \"300\": \"three_hundred\"," + " \"400\": \"four_hundred\"" + "}")); + std::map mixed_key_map{ + std::make_pair(100, "int_value"), + std::make_pair(3.5, "double_value"), + std::make_pair(true, "boolean_value"), + std::make_pair("Good-bye, World!", "string_value"), + }; + EXPECT_THAT(VariantToJson(mixed_key_map), + EqualsJson("{" + " \"100\": \"int_value\"," + " \"3.5000000000000000\": \"double_value\"," + " \"true\": \"boolean_value\"," + " \"Good-bye, World!\": \"string_value\"" + "}")); +} + +TEST(UtilDesktopTest, VariantToJsonMapWithBadKeys) { + // JSON only supports strings for keys (and this implmentation will coerce + // fundamental types to string keys. Anything else (containers, blobs) + // should fail, which is represented by an empty string. Also, the empty + // string is not valid JSON, so we must test with StrEq instead of + // JsonEquals. + + // Vector as a key. + std::vector int_vector{1, 2, 3, 4}; + std::map map_with_vector_key{ + std::make_pair(int_vector, "pairs of numbers!"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_vector_key)), StrEq("")); + + // Map as a key. + std::map int_map{ + std::make_pair(1, 1), + std::make_pair(2, 3), + std::make_pair(5, 8), + std::make_pair(13, 21), + }; + std::map map_with_map_key{ + std::make_pair(int_map, "pairs of numbers!"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_map_key)), StrEq("")); + + std::string blob_data = "abcdefghijklmnopqrstuvwxyz"; + + // Static blob as a key. + Variant static_blob = + Variant::FromStaticBlob(blob_data.c_str(), blob_data.size()); + std::map map_with_static_blob_key{ + std::make_pair(static_blob, "blobby blob blob"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_static_blob_key)), StrEq("")); + + // Mutable blob as a key. + Variant mutable_blob = + Variant::FromMutableBlob(blob_data.c_str(), blob_data.size()); + std::map map_with_mutable_blob_key{ + std::make_pair(static_blob, "blorby blorb blorb"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_mutable_blob_key)), StrEq("")); + + // Legal top level map with illegal nested values. + std::map map_with_legal_key{ + std::make_pair("totes legal", map_with_map_key)}; + EXPECT_THAT(VariantToJson(Variant(map_with_legal_key)), StrEq("")); +} + +TEST(UtilDesktopTest, VariantToJsonWithStaticBlob) { + // Static blobs are not supported, so we expect these to fail, which is + // represented by an empty string. + std::string blob_data = "abcdefghijklmnopqrstuvwxyz"; + Variant blob = Variant::FromStaticBlob(blob_data.c_str(), blob_data.size()); + EXPECT_THAT(VariantToJson(blob), StrEq("")); + std::vector blob_vector{1, true, 3.5, "hello", blob}; + EXPECT_THAT(VariantToJson(blob_vector), StrEq("")); + std::map blob_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + std::make_pair("blob_value", blob), + }; + EXPECT_THAT(VariantToJson(blob_map), StrEq("")); +} + +TEST(UtilDesktopTest, VariantToJsonWithMutableBlob) { + // Mutable blobs are not supported, so we expect these to fail, which is + // represented by an empty string. + std::string blob_data = "abcdefghijklmnopqrstuvwxyz"; + Variant blob = Variant::FromMutableBlob(blob_data.c_str(), blob_data.size()); + EXPECT_THAT(VariantToJson(blob), StrEq("")); + std::vector blob_vector{1, true, 3.5, "hello", blob}; + EXPECT_THAT(VariantToJson(blob_vector), StrEq("")); + std::map blob_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + std::make_pair("blob_value", blob), + }; + EXPECT_THAT(VariantToJson(blob_map), StrEq("")); +} + +TEST(UtilDesktopTest, VariantToFlexbufferNull) { + EXPECT_TRUE(GetRoot(VariantToFlexbuffer(Variant::Null())).IsNull()); +} + +TEST(UtilDesktopTest, VariantToFlexbufferInt64) { + EXPECT_THAT(GetRoot(VariantToFlexbuffer(0)).AsInt32(), Eq(0)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(100)).AsInt32(), Eq(100)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(int64_t(8000000000L))).AsInt64(), + Eq(8000000000)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(-100)).AsInt32(), Eq(-100)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(int64_t(-8000000000L))).AsInt64(), + Eq(-8000000000)); +} + +TEST(UtilDesktopTest, VariantToFlexbufferDouble) { + EXPECT_THAT(GetRoot(VariantToFlexbuffer(0.0)).AsDouble(), Eq(0.0)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(100.0)).AsDouble(), Eq(100.0)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(-100.0)).AsDouble(), Eq(-100.0)); +} + +TEST(UtilDesktopTest, VariantToFlexbufferBool) { + EXPECT_TRUE(GetRoot(VariantToFlexbuffer(Variant::True())).AsBool()); + EXPECT_FALSE(GetRoot(VariantToFlexbuffer(Variant::False())).AsBool()); +} + +TEST(UtilDesktopTest, VariantToFlexbufferString) { + EXPECT_THAT(GetRoot(VariantToFlexbuffer("Hello, World!")).AsString().c_str(), + StrEq("Hello, World!")); + EXPECT_THAT(GetRoot(VariantToFlexbuffer("100")).AsString().c_str(), + StrEq("100")); + EXPECT_THAT(GetRoot(VariantToFlexbuffer("false")).AsString().c_str(), + StrEq("false")); +} + +TEST(UtilDesktopTest, VariantToFlexbufferVector) { + flexbuffers::Builder fbb(512); + fbb.Vector([&]() {}); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(Variant::EmptyVector()), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::vector int_vector{1, 2, 3, 4}; + fbb.Vector([&]() { + fbb += 1; + fbb += 2; + fbb += 3; + fbb += 4; + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(int_vector), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::vector mixed_vector{1, true, 3.5, "hello"}; + fbb.Vector([&]() { + fbb += 1; + fbb += true; + fbb += 3.5; + fbb += "hello"; + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(mixed_vector), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::vector nested_vector{1, true, 3.5, "hello", int_vector}; + fbb.Vector([&]() { + fbb += 1; + fbb += true; + fbb += 3.5; + fbb += "hello"; + fbb.Vector([&]() { + fbb += 1; + fbb += 2; + fbb += 3; + fbb += 4; + }); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(nested_vector), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); +} + +TEST(UtilDesktopTest, VariantToFlexbufferMapWithStringKeys) { + flexbuffers::Builder fbb(512); + fbb.Map([&]() {}); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(Variant::EmptyMap()), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::map int_map{ + std::make_pair("one_hundred", 100), + std::make_pair("two_hundred", 200), + std::make_pair("three_hundred", 300), + std::make_pair("four_hundred", 400), + }; + fbb.Map([&]() { + fbb.Add("one_hundred", 100); + fbb.Add("two_hundred", 200); + fbb.Add("three_hundred", 300); + fbb.Add("four_hundred", 400); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(int_map), EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::map mixed_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + }; + fbb.Map([&]() { + fbb.Add("int_value", 100); + fbb.Add("boolean_value", true); + fbb.Add("double_value", 3.5); + fbb.Add("string_value", "Good-bye, World!"); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(mixed_map), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::map nested_map{ + std::make_pair("int_map", int_map), + std::make_pair("mixed_map", mixed_map), + }; + fbb.Map([&]() { + fbb.Map("int_map", [&]() { + fbb.Add("one_hundred", 100); + fbb.Add("two_hundred", 200); + fbb.Add("three_hundred", 300); + fbb.Add("four_hundred", 400); + }); + fbb.Map("mixed_map", [&]() { + fbb.Add("int_value", 100); + fbb.Add("boolean_value", true); + fbb.Add("double_value", 3.5); + fbb.Add("string_value", "Good-bye, World!"); + }); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(nested_map), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); +} + +} // namespace diff --git a/auth/src/ios/fake/FIRActionCodeSettings.h b/auth/src/ios/fake/FIRActionCodeSettings.h new file mode 100644 index 0000000000..cb7528cc7f --- /dev/null +++ b/auth/src/ios/fake/FIRActionCodeSettings.h @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Google + * + * 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/LICENSE2.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. + */ + + #import + + NS_ASSUME_NONNULL_BEGIN + + /** @class FIRActionCodeSettings + @brief Used to set and retrieve settings related to handling action codes. + */ + NS_SWIFT_NAME(ActionCodeSettings) + @interface FIRActionCodeSettings : NSObject + + /** @property URL + @brief This URL represents the state/Continue URL in the form of a universal link. + @remarks This URL can should be contructed as a universal link that would either directly open + the app where the action code would be handled or continue to the app after the action code + is handled by Firebase. + */ + @property(nonatomic, copy, nullable) NSURL *URL; + + /** @property handleCodeInApp + @brief Indicates whether the action code link will open the app directly or after being + redirected from a Firebase owned web widget. + */ + @property(assign, nonatomic) BOOL handleCodeInApp; + + /** @property iOSBundleID + @brief The iOS bundle ID, if available. The default value is the current app's bundle ID. + */ + @property(copy, nonatomic, readonly, nullable) NSString *iOSBundleID; + + /** @property androidPackageName + @brief The Android package name, if available. + */ + @property(nonatomic, copy, readonly, nullable) NSString *androidPackageName; + + /** @property androidMinimumVersion + @brief The minimum Android version supported, if available. + */ + @property(nonatomic, copy, readonly, nullable) NSString *androidMinimumVersion; + + /** @property androidInstallIfNotAvailable + @brief Indicates whether the Android app should be installed on a device where it is not + available. + */ + @property(nonatomic, assign, readonly) BOOL androidInstallIfNotAvailable; + + /** @property dynamicLinkDomain + @brief The Firebase Dynamic Link domain used for out of band code flow. + */ + @property(copy, nonatomic, nullable) NSString *dynamicLinkDomain; + + /** @fn setIOSBundleID + @brief Sets the iOS bundle Id. + @param iOSBundleID The iOS bundle ID. + */ + - (void)setIOSBundleID:(NSString *)iOSBundleID; + + /** @fn setAndroidPackageName:installIfNotAvailable:minimumVersion: + @brief Sets the Android package name, the flag to indicate whether or not to install the app + and the minimum Android version supported. + @param androidPackageName The Android package name. + @param installIfNotAvailable Indicates whether or not the app should be installed if not + available. + @param minimumVersion The minimum version of Android supported. + @remarks If installIfNotAvailable is set to YES and the link is opened on an android device, it + will try to install the app if not already available. Otherwise the web URL is used. + */ + - (void)setAndroidPackageName:(NSString *)androidPackageName + installIfNotAvailable:(BOOL)installIfNotAvailable + minimumVersion:(nullable NSString *)minimumVersion; + + @end + + NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAdditionalUserInfo.h b/auth/src/ios/fake/FIRAdditionalUserInfo.h new file mode 100644 index 0000000000..2e57ff20f4 --- /dev/null +++ b/auth/src/ios/fake/FIRAdditionalUserInfo.h @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRVerifyAssertionResponse; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAdditionalUserInfo + @brief Represents additional user data returned from an identity provider. + */ +NS_SWIFT_NAME(AdditionalUserInfo) +@interface FIRAdditionalUserInfo : NSObject + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be initialized manually. `FIRAdditionalUserInfo` can be retrieved + from from an instance of `FIRAuthDataResult`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @property providerID + @brief The provider identifier. + */ +@property(nonatomic, readonly) NSString *providerID; + +/** @property profile + @brief Dictionary containing the additional IdP specific information. + */ +@property(nonatomic, readonly, nullable) NSDictionary *profile; + +/** @property username + @brief username The name of the user. + */ +@property(nonatomic, readonly, nullable) NSString *username; + +/** @property newUser + @brief Indicates whether or not the current user was signed in for the first time. + */ +@property(nonatomic, readonly, getter=isNewUser) BOOL newUser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAdditionalUserInfo.mm b/auth/src/ios/fake/FIRAdditionalUserInfo.mm new file mode 100644 index 0000000000..1e37b3fa8e --- /dev/null +++ b/auth/src/ios/fake/FIRAdditionalUserInfo.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAdditionalUserInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAdditionalUserInfo + +- (instancetype)init { + self = [super init]; + if (self) { + _providerID = @"fake provider id"; + _profile = nil; + _username = @"fake user name"; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuth.h b/auth/src/ios/fake/FIRAuth.h new file mode 100644 index 0000000000..29cf4320a6 --- /dev/null +++ b/auth/src/ios/fake/FIRAuth.h @@ -0,0 +1,832 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import +#import + +#if TARGET_OS_IOS +#import "auth/src/ios/fake/FIRAuthAPNSTokenType.h" +#endif + +@class FIRActionCodeSettings; +@class FIRApp; +@class FIRAuth; +@class FIRAuthCredential; +@class FIRAuthDataResult; +@class FIRAuthSettings; +@class FIRUser; +@protocol FIRAuthStateListener; +@protocol FIRAuthUIDelegate; +@protocol FIRFederatedAuthProvider; + +NS_ASSUME_NONNULL_BEGIN + +/** @typedef FIRUserUpdateCallback + @brief The type of block invoked when a request to update the current user is completed. + */ +typedef void (^FIRUserUpdateCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(UserUpdateCallback); + +/** @typedef FIRAuthStateDidChangeListenerHandle + @brief The type of handle returned by `FIRAuth.addAuthStateDidChangeListener:`. + */ +typedef id FIRAuthStateDidChangeListenerHandle + NS_SWIFT_NAME(AuthStateDidChangeListenerHandle); + +/** @typedef FIRAuthStateDidChangeListenerBlock + @brief The type of block which can be registered as a listener for auth state did change events. + + @param auth The FIRAuth object on which state changes occurred. + @param user Optionally; the current signed in user, if any. + */ +typedef void(^FIRAuthStateDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user) + NS_SWIFT_NAME(AuthStateDidChangeListenerBlock); + +/** @typedef FIRIDTokenDidChangeListenerHandle + @brief The type of handle returned by `FIRAuth.addIDTokenDidChangeListener:`. + */ +typedef id FIRIDTokenDidChangeListenerHandle + NS_SWIFT_NAME(IDTokenDidChangeListenerHandle); + +/** @typedef FIRIDTokenDidChangeListenerBlock + @brief The type of block which can be registered as a listener for ID token did change events. + + @param auth The FIRAuth object on which ID token changes occurred. + @param user Optionally; the current signed in user, if any. + */ +typedef void(^FIRIDTokenDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user) + NS_SWIFT_NAME(IDTokenDidChangeListenerBlock); + +/** @typedef FIRAuthDataResultCallback + @brief The type of block invoked when sign-in related events complete. + + @param authResult Optionally; Result of sign-in request containing both the user and + the additional user info associated with the user. + @param error Optionally; the error which occurred - or nil if the request was successful. + */ +typedef void (^FIRAuthDataResultCallback)(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthDataResultCallback); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + @brief The name of the `NSNotificationCenter` notification which is posted when the auth state + changes (for example, a new token has been produced, a user signs in or signs out). The + object parameter of the notification is the sender `FIRAuth` instance. + */ +extern const NSNotificationName FIRAuthStateDidChangeNotification + NS_SWIFT_NAME(AuthStateDidChange); +#else +/** + @brief The name of the `NSNotificationCenter` notification which is posted when the auth state + changes (for example, a new token has been produced, a user signs in or signs out). The + object parameter of the notification is the sender `FIRAuth` instance. + */ +extern NSString *const FIRAuthStateDidChangeNotification + NS_SWIFT_NAME(AuthStateDidChangeNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** @typedef FIRAuthResultCallback + @brief The type of block invoked when sign-in related events complete. + + @param user Optionally; the signed in user, if any. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRAuthResultCallback)(FIRUser *_Nullable user, NSError *_Nullable error) + NS_SWIFT_NAME(AuthResultCallback); + +/** @typedef FIRProviderQueryCallback + @brief The type of block invoked when a list of identity providers for a given email address is + requested. + + @param providers Optionally; a list of provider identifiers, if any. + @see FIRGoogleAuthProviderID etc. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRProviderQueryCallback)(NSArray *_Nullable providers, + NSError *_Nullable error) + NS_SWIFT_NAME(ProviderQueryCallback); + +/** @typedef FIRSignInMethodQueryCallback + @brief The type of block invoked when a list of sign-in methods for a given email address is + requested. + */ +typedef void (^FIRSignInMethodQueryCallback)(NSArray *_Nullable, + NSError *_Nullable) + NS_SWIFT_NAME(SignInMethodQueryCallback); + +/** @typedef FIRSendPasswordResetCallback + @brief The type of block invoked when sending a password reset email. + + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRSendPasswordResetCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendPasswordResetCallback); + +/** @typedef FIRSendSignInLinkToEmailCallback + @brief The type of block invoked when sending an email sign-in link email. + */ +typedef void (^FIRSendSignInLinkToEmailCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendSignInLinkToEmailCallback); + +/** @typedef FIRConfirmPasswordResetCallback + @brief The type of block invoked when performing a password reset. + + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRConfirmPasswordResetCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(ConfirmPasswordResetCallback); + +/** @typedef FIRVerifyPasswordResetCodeCallback + @brief The type of block invoked when verifying that an out of band code should be used to + perform password reset. + + @param email Optionally; the email address of the user for which the out of band code applies. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRVerifyPasswordResetCodeCallback)(NSString *_Nullable email, + NSError *_Nullable error) + NS_SWIFT_NAME(VerifyPasswordResetCodeCallback); + +/** @typedef FIRApplyActionCodeCallback + @brief The type of block invoked when applying an action code. + + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRApplyActionCodeCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(ApplyActionCodeCallback); + +/** + @brief Keys used to retrieve operation data from a `FIRActionCodeInfo` object by the + `dataForKey` method. + */ +typedef NS_ENUM(NSInteger, FIRActionDataKey) { + /** + * The email address to which the code was sent. + * For FIRActionCodeOperationRecoverEmail, the new email address for the account. + */ + FIRActionCodeEmailKey = 0, + + /** For FIRActionCodeOperationRecoverEmail, the current email address for the account. */ + FIRActionCodeFromEmailKey = 1 +} NS_SWIFT_NAME(ActionDataKey); + +/** @class FIRActionCodeInfo + @brief Manages information regarding action codes. + */ +NS_SWIFT_NAME(ActionCodeInfo) +@interface FIRActionCodeInfo : NSObject + +/** + @brief Operations which can be performed with action codes. + */ +typedef NS_ENUM(NSInteger, FIRActionCodeOperation) { + /** Action code for unknown operation. */ + FIRActionCodeOperationUnknown = 0, + + /** Action code for password reset operation. */ + FIRActionCodeOperationPasswordReset = 1, + + /** Action code for verify email operation. */ + FIRActionCodeOperationVerifyEmail = 2, + + /** Action code for recover email operation. */ + FIRActionCodeOperationRecoverEmail = 3, + + /** Action code for email link operation. */ + FIRActionCodeOperationEmailLink = 4, + + +} NS_SWIFT_NAME(ActionCodeOperation); + +/** + @brief The operation being performed. + */ +@property(nonatomic, readonly) FIRActionCodeOperation operation; + +/** @fn dataForKey: + @brief The operation being performed. + + @param key The FIRActionDataKey value used to retrieve the operation data. + + @return The operation data pertaining to the provided action code key. + */ +- (NSString *)dataForKey:(FIRActionDataKey)key; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief please use initWithOperation: instead. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +@end + +/** @typedef FIRCheckActionCodeCallBack + @brief The type of block invoked when performing a check action code operation. + + @param info Metadata corresponding to the action code. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRCheckActionCodeCallBack)(FIRActionCodeInfo *_Nullable info, + NSError *_Nullable error) + NS_SWIFT_NAME(CheckActionCodeCallback); + +/** @class FIRAuth + @brief Manages authentication for Firebase apps. + @remarks This class is thread-safe. + */ +NS_SWIFT_NAME(Auth) +@interface FIRAuth : NSObject + +/** @fn auth + @brief Gets the auth object for the default Firebase app. + @remarks The default Firebase app must have already been configured or an exception will be + raised. + */ ++ (FIRAuth *)auth NS_SWIFT_NAME(auth()); + +/** @fn authWithApp: + @brief Gets the auth object for a `FIRApp`. + + @param app The FIRApp for which to retrieve the associated FIRAuth instance. + @return The FIRAuth instance associated with the given FIRApp. + */ ++ (FIRAuth *)authWithApp:(FIRApp *)app NS_SWIFT_NAME(auth(app:)); + +/** @property app + @brief Gets the `FIRApp` object that this auth object is connected to. + */ +@property(nonatomic, weak, readonly, nullable) FIRApp *app; + +/** @property currentUser + @brief Synchronously gets the cached current user, or null if there is none. + */ +@property(nonatomic, strong, readonly, nullable) FIRUser *currentUser; + +/** @property languageCode + @brief The current user language code. This property can be set to the app's current language by + calling `useAppLanguage`. + + @remarks The string used to set this property must be a language code that follows BCP 47. + */ +@property(nonatomic, copy, nullable) NSString *languageCode; + +/** @property settings + @brief Contains settings related to the auth object. + */ +@property(nonatomic, copy, nullable) FIRAuthSettings *settings; + +/** @property userAccessGroup + @brief The current user access group that the Auth instance is using. Default is nil. + */ +@property(readonly, nonatomic, copy, nullable) NSString *userAccessGroup; + +#if TARGET_OS_IOS +/** @property APNSToken + @brief The APNs token used for phone number authentication. The type of the token (production + or sandbox) will be attempted to be automatcially detected. + @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work, + by either setting this property or by calling `setAPNSToken:type:` + */ +@property(nonatomic, strong, nullable) NSData *APNSToken; +#endif + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief Please access auth instances using `FIRAuth.auth` and `FIRAuth.authForApp:`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @fn updateCurrentUser:completion: + @brief Sets the currentUser on the calling Auth instance to the provided user object. + @param user The user object to be set as the current user of the calling Auth instance. + @param completion Optionally; a block invoked after the user of the calling Auth instance has + been updated or an error was encountered. + */ +- (void)updateCurrentUser:(FIRUser *)user completion:(nullable FIRUserUpdateCallback)completion; + +/** @fn fetchProvidersForEmail:completion: + @brief Please use fetchSignInMethodsForEmail:completion: for Objective-C or + fetchSignInMethods(forEmail:completion:) for Swift instead. + */ +- (void)fetchProvidersForEmail:(NSString *)email + completion:(nullable FIRProviderQueryCallback)completion +DEPRECATED_MSG_ATTRIBUTE("Please use fetchSignInMethodsForEmail:completion: for Objective-C or " + "fetchSignInMethods(forEmail:completion:) for Swift instead."); + +/** @fn fetchSignInMethodsForEmail:completion: + @brief Fetches the list of all sign-in methods previously used for the provided email address. + + @param email The email address for which to obtain a list of sign-in methods. + @param completion Optionally; a block which is invoked when the list of sign in methods for the + specified email address is ready or an error was encountered. Invoked asynchronously on the + main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods. + */ + +- (void)fetchSignInMethodsForEmail:(NSString *)email + completion:(nullable FIRSignInMethodQueryCallback)completion; + +/** @fn signInWithEmail:password:completion: + @brief Signs in using an email address and password. + + @param email The user's email address. + @param password The user's password. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and password + accounts are not enabled. Enable them in the Auth section of the + Firebase console. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted + sign in with an incorrect password. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)signInWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInWithEmail:link:completion: + @brief Signs in using an email address and email sign-in link. + + @param email The user's email address. + @param link The email sign-in link. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and email sign-in link + accounts are not enabled. Enable them in the Auth section of the + Firebase console. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is invalid. + + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ + +- (void)signInWithEmail:(NSString *)email + link:(NSString *)link + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInWithProvider:UIDelegate:completion: + @brief Signs in using the provided auth provider instance. + + @param provider An instance of an auth provider used to initiate the sign-in flow. + @param UIDelegate Optionally an instance of a class conforming to the FIRAuthUIDelegate + protocol, this is used for presenting the web context. If nil, a default FIRAuthUIDelegate + will be used. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: +
      +
    • @c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password + accounts are not enabled. Enable them in the Auth section of the + Firebase console. +
    • +
    • @c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled. +
    • +
    • @c FIRAuthErrorCodeWebNetworkRequestFailed - Indicates that a network request within a + SFSafariViewController or UIWebview failed. +
    • +
    • @c FIRAuthErrorCodeWebInternalError - Indicates that an internal error occurred within a + SFSafariViewController or UIWebview. +
    • +
    • @c FIRAuthErrorCodeWebSignInUserInteractionFailure - Indicates a general failure during + a web sign-in flow. +
    • +
    • @c FIRAuthErrorCodeWebContextAlreadyPresented - Indicates that an attempt was made to + present a new web context while one was already being presented. +
    • +
    • @c FIRAuthErrorCodeWebContextCancelled - Indicates that the URL presentation was + cancelled prematurely by the user. +
    • +
    • @c FIRAuthErrorCodeAccountExistsWithDifferentCredential - Indicates the email asserted + by the credential (e.g. the email in a Facebook access token) is already in use by an + existing account, that cannot be authenticated with this sign-in method. Call + fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + the sign-in providers returned. This error will only be thrown if the "One account per + email address" setting is enabled in the Firebase console, under Auth settings. +
    • +
    + + @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods. + */ +- (void)signInWithProvider:(id)provider + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInAndRetrieveDataWithCredential:completion: + @brief Please use signInWithCredential:completion: for Objective-C or " + "signIn(with:completion:) for Swift instead. + */ +- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion +DEPRECATED_MSG_ATTRIBUTE("Please use signInWithCredential:completion: for Objective-C or " + "signIn(with:completion:) for Swift instead."); + +/** @fn signInWithCredential:completion: + @brief Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook + login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional + identity provider data. + + @param credential The credential supplied by the IdP. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + This could happen if it has expired or it is malformed. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that accounts + with the identity provider represented by the credential are not enabled. + Enable them in the Auth section of the Firebase console. + + `FIRAuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted + by the credential (e.g. the email in a Facebook access token) is already in use by an + existing account, that cannot be authenticated with this sign-in method. Call + fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + the sign-in providers returned. This error will only be thrown if the "One account per + email address" setting is enabled in the Firebase console, under Auth settings. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted sign in with an + incorrect password, if credential is of the type EmailPasswordAuthCredential. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + `FIRAuthErrorCodeMissingVerificationID` - Indicates that the phone auth credential was + created with an empty verification ID. + + `FIRAuthErrorCodeMissingVerificationCode` - Indicates that the phone auth credential + was created with an empty verification code. + + `FIRAuthErrorCodeInvalidVerificationCode` - Indicates that the phone auth credential + was created with an invalid verification Code. + + `FIRAuthErrorCodeInvalidVerificationID` - Indicates that the phone auth credential was + created with an invalid verification ID. + + `FIRAuthErrorCodeSessionExpired` - Indicates that the SMS code has expired. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods +*/ +- (void)signInWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInAnonymouslyWithCompletion: + @brief Asynchronously creates and becomes an anonymous user. + @param completion Optionally; a block which is invoked when the sign in finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks If there is already an anonymous user signed in, that user will be returned instead. + If there is any other existing user signed in, that user will be signed out. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are + not enabled. Enable them in the Auth section of the Firebase console. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInWithCustomToken:completion: + @brief Asynchronously signs in to Firebase with the given Auth token. + + @param token A self-signed custom auth token. + @param completion Optionally; a block which is invoked when the sign in finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidCustomToken` - Indicates a validation error with + the custom token. + + `FIRAuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key + belong to different projects. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)signInWithCustomToken:(NSString *)token + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn createUserWithEmail:password:completion: + @brief Creates and, on success, signs in a user with the given email address and password. + + @param email The user's email address. + @param password The user's desired password. + @param completion Optionally; a block which is invoked when the sign up flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up + already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user + used, and prompt the user to sign in with one of those. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts + are not enabled. Enable them in the Auth section of the Firebase console. + + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo + dictionary object will contain more detailed explanation that can be shown to the user. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)createUserWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn confirmPasswordResetWithCode:newPassword:completion: + @brief Resets the password given a code sent to the user outside of the app and a new password + for the user. + + @param newPassword The new password. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + considered too weak. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled sign + in with the specified identity provider. + + `FIRAuthErrorCodeExpiredActionCode` - Indicates the OOB code is expired. + + `FIRAuthErrorCodeInvalidActionCode` - Indicates the OOB code is invalid. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)confirmPasswordResetWithCode:(NSString *)code + newPassword:(NSString *)newPassword + completion:(FIRConfirmPasswordResetCallback)completion; + +/** @fn checkActionCode:completion: + @brief Checks the validity of an out of band code. + + @param code The out of band code to check validity. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion; + +/** @fn verifyPasswordResetCode:completion: + @brief Checks the validity of a verify password reset code. + + @param code The password reset code to be verified. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)verifyPasswordResetCode:(NSString *)code + completion:(FIRVerifyPasswordResetCodeCallback)completion; + +/** @fn applyActionCode:completion: + @brief Applies out of band code. + + @param code The out of band code to be applied. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks This method will not work for out of band codes which require an additional parameter, + such as password reset code. + */ +- (void)applyActionCode:(NSString *)code + completion:(FIRApplyActionCodeCallback)completion; + +/** @fn sendPasswordResetWithEmail:completion: + @brief Initiates a password reset for the given email address. + + @param email The email address of the user. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + + */ +- (void)sendPasswordResetWithEmail:(NSString *)email + completion:(nullable FIRSendPasswordResetCallback)completion; + +/** @fn sendPasswordResetWithEmail:actionCodeSetting:completion: + @brief Initiates a password reset for the given email address and @FIRActionCodeSettings object. + + @param email The email address of the user. + @param actionCodeSettings An `FIRActionCodeSettings` object containing settings related to + handling action codes. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when + `handleCodeInApp` is set to YES. + + `FIRAuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name + is missing when the `androidInstallApp` flag is set to true. + + `FIRAuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the + continue URL is not whitelisted in the Firebase console. + + `FIRAuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the + continue URI is not valid. + + + */ + - (void)sendPasswordResetWithEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendPasswordResetCallback)completion; + +/** @fn sendSignInLinkToEmail:actionCodeSettings:completion: + @brief Sends a sign in with email link to provided email address. + + @param email The email address of the user. + @param actionCodeSettings An `FIRActionCodeSettings` object containing settings related to + handling action codes. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)sendSignInLinkToEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendSignInLinkToEmailCallback)completion; + +/** @fn signOut: + @brief Signs out the current user. + + @param error Optionally; if an error occurs, upon return contains an NSError object that + describes the problem; is nil otherwise. + @return @YES when the sign out request was successful. @NO otherwise. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeKeychainError` - Indicates an error occurred when accessing the + keychain. The `NSLocalizedFailureReasonErrorKey` field in the `NSError.userInfo` + dictionary will contain more information about the error encountered. + + + + */ +- (BOOL)signOut:(NSError *_Nullable *_Nullable)error; + +/** @fn isSignInWithEmailLink + @brief Checks if link is an email sign-in link. + + @param link The email sign-in link. + @return @YES when the link passed matches the expected format of an email sign-in link. + */ +- (BOOL)isSignInWithEmailLink:(NSString *)link; + +/** @fn addAuthStateDidChangeListener: + @brief Registers a block as an "auth state did change" listener. To be invoked when: + + + The block is registered as a listener, + + A user with a different UID from the current user has signed in, or + + The current user has signed out. + + @param listener The block to be invoked. The block is always invoked asynchronously on the main + thread, even for it's initial invocation after having been added as a listener. + + @remarks The block is invoked immediately after adding it according to it's standard invocation + semantics, asynchronously on the main thread. Users should pay special attention to + making sure the block does not inadvertently retain objects which should not be retained by + the long-lived block. The block itself will be retained by `FIRAuth` until it is + unregistered or until the `FIRAuth` instance is otherwise deallocated. + + @return A handle useful for manually unregistering the block as a listener. + */ +- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener: + (FIRAuthStateDidChangeListenerBlock)listener; + +/** @fn removeAuthStateDidChangeListener: + @brief Unregisters a block as an "auth state did change" listener. + + @param listenerHandle The handle for the listener. + */ +- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle; + +/** @fn addIDTokenDidChangeListener: + @brief Registers a block as an "ID token did change" listener. To be invoked when: + + + The block is registered as a listener, + + A user with a different UID from the current user has signed in, + + The ID token of the current user has been refreshed, or + + The current user has signed out. + + @param listener The block to be invoked. The block is always invoked asynchronously on the main + thread, even for it's initial invocation after having been added as a listener. + + @remarks The block is invoked immediately after adding it according to it's standard invocation + semantics, asynchronously on the main thread. Users should pay special attention to + making sure the block does not inadvertently retain objects which should not be retained by + the long-lived block. The block itself will be retained by `FIRAuth` until it is + unregistered or until the `FIRAuth` instance is otherwise deallocated. + + @return A handle useful for manually unregistering the block as a listener. + */ +- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener: + (FIRIDTokenDidChangeListenerBlock)listener; + +/** @fn removeIDTokenDidChangeListener: + @brief Unregisters a block as an "ID token did change" listener. + + @param listenerHandle The handle for the listener. + */ +- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle; + +/** @fn useAppLanguage + @brief Sets `languageCode` to the app's current language. + */ +- (void)useAppLanguage; + +#if TARGET_OS_IOS + +/** @fn canHandleURL: + @brief Whether the specific URL is handled by `FIRAuth` . + @param URL The URL received by the application delegate from any of the openURL method. + @return Whether or the URL is handled. YES means the URL is for Firebase Auth + so the caller should ignore the URL from further processing, and NO means the + the URL is for the app (or another libaray) so the caller should continue handling + this URL as usual. + @remarks If swizzling is disabled, URLs received by the application delegate must be forwarded + to this method for phone number auth to work. + */ +- (BOOL)canHandleURL:(nonnull NSURL *)URL; + +/** @fn setAPNSToken:type: + @brief Sets the APNs token along with its type. + @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work, + by either setting calling this method or by setting the `APNSToken` property. + */ +- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type; + +/** @fn canHandleNotification: + @brief Whether the specific remote notification is handled by `FIRAuth` . + @param userInfo A dictionary that contains information related to the + notification in question. + @return Whether or the notification is handled. YES means the notification is for Firebase Auth + so the caller should ignore the notification from further processing, and NO means the + the notification is for the app (or another libaray) so the caller should continue handling + this notification as usual. + @remarks If swizzling is disabled, related remote notifications must be forwarded to this method + for phone number auth to work. + */ +- (BOOL)canHandleNotification:(NSDictionary *)userInfo; + +#endif // TARGET_OS_IOS + +#pragma mark - User sharing + +/** @fn useUserAccessGroup:error: + @brief Switch userAccessGroup and current user to the given accessGroup and the user stored in + it. + */ +- (BOOL)useUserAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError; + +/** @fn getStoredUserForAccessGroup:error: + @brief Get the stored user in the given accessGroup. + */ +- (nullable FIRUser *)getStoredUserForAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuth.mm b/auth/src/ios/fake/FIRAuth.mm new file mode 100644 index 0000000000..9c51dadae9 --- /dev/null +++ b/auth/src/ios/fake/FIRAuth.mm @@ -0,0 +1,214 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuth.h" +#import "auth/src/ios/fake/FIRAuthErrors.h" +#import "auth/src/ios/fake/FIRAuthDataResult.h" +#import "auth/src/ios/fake/FIRAuthUIDelegate.h" +#import "auth/src/ios/fake/FIRUser.h" + +#include +#include +#include "testing/util_ios.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const FIRAuthErrorUserInfoUpdatedCredentialKey = + @"FIRAuthErrorUserInfoUpdatedCredentialKey"; + +@implementation FIRAuth { + // Manages callbacks for testing. + firebase::testing::cppsdk::CallbackTickerManager _callbackManager; +} + +- (instancetype)init { + return [super init]; +} + ++ (FIRAuth *)auth { + return [[FIRAuth alloc] init]; +} + ++ (FIRAuth *)authWithApp:(FIRApp *)app { + FIRAuth *result = [[FIRAuth alloc] init]; + return result; +} + +static int AuthErrorFromConfig(const char *config_key) { + const firebase::testing::cppsdk::ConfigRow *row = + firebase::testing::cppsdk::ConfigGet(config_key); + if (row != nullptr && row->futuregeneric()->throwexception()) { + std::regex expression("^\\[.*[?!:]:?(.*)\\].*"); + std::smatch result; + std::string search_str(row->futuregeneric()->exceptionmsg()->c_str()); + if (std::regex_search(search_str, result, expression)) { + // The messages that throw errors should have: + // "[AndroidNamedException:ERROR_FIREBASE_PROBLEM] ". + // result.str(1) contains the "ERROR_FIREBASE_PROBLEM" part. + // The mapping between ios, android, and generic firebase errors is here: + // https://docs.google.com/spreadsheets/d/1U5ESSHoc10Vd7sDoQO-CbbQ46_ThGol2lhViFs8Eg2g/ + std::string error_code = result.str(1); + if (error_code == "ERROR_INVALID_CUSTOM_TOKEN") return FIRAuthErrorCodeInvalidCustomToken; + if (error_code == "ERROR_INVALID_EMAIL") return FIRAuthErrorCodeInvalidEmail; + if (error_code == "ERROR_OPERATION_NOT_ALLOWED") return FIRAuthErrorCodeOperationNotAllowed; + if (error_code == "ERROR_WRONG_PASSWORD") return FIRAuthErrorCodeWrongPassword; + if (error_code == "ERROR_EMAIL_ALREADY_IN_USE") return FIRAuthErrorCodeEmailAlreadyInUse; + if (error_code == "ERROR_INVALID_MESSAGE_PAYLOAD") + return FIRAuthErrorCodeInvalidMessagePayload; + } + } + return -1; +} + +- (void)updateCurrentUser:(FIRUser *)user completion:(nullable FIRUserUpdateCallback)completion {} + +- (void)fetchProvidersForEmail:(NSString *)email {} + +- (void)fetchProvidersForEmail:(NSString *)email + completion:(nullable FIRProviderQueryCallback)completion {} + +- (void)fetchSignInMethodsForEmail:(NSString *)email + completion:(nullable FIRSignInMethodQueryCallback)completion {} + +- (void)signInWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInWithEmail:password:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithEmail:password:completion:")); +} + +- (void)signInWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithCredential:completion:")); +} + +- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add( + @"FIRAuth.signInAndRetrieveDataWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInAndRetrieveDataWithCredential:completion:")); +} + +- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInAnonymouslyWithCompletion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInAnonymouslyWithCompletion:")); +} + +- (void)signInWithCustomToken:(NSString *)token + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInWithCustomToken:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithCustomToken:completion:")); +} + +- (void)signInWithEmail:(NSString *)email + link:(NSString *)link + completion:(nullable FIRAuthDataResultCallback)completion {} + +- (void)signInWithProvider:(id)provider + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthDataResultCallback)completion { + + _callbackManager.Add(@"FIRAuth.signInWithProvider:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithProvider:completion:")); +} + +- (void)createUserWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.createUserWithEmail:password:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.createUserWithEmail:password:completion:")); +} + +- (void)confirmPasswordResetWithCode:(NSString *)code + newPassword:(NSString *)newPassword + completion:(FIRConfirmPasswordResetCallback)completion {} + +- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion {} + +- (void)verifyPasswordResetCode:(NSString *)code + completion:(FIRVerifyPasswordResetCodeCallback)completion {} + +- (void)applyActionCode:(NSString *)code + completion:(FIRApplyActionCodeCallback)completion {} + +- (void)sendPasswordResetWithEmail:(NSString *)email + completion:(nullable FIRSendPasswordResetCallback)completion { + _callbackManager.Add(@"FIRAuth.sendPasswordResetWithEmail:completion:", completion, + AuthErrorFromConfig("FIRAuth.sendPasswordResetWithEmail:completion:")); +} + +- (void)sendPasswordResetWithEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendPasswordResetCallback)completion {} + +- (void)sendSignInLinkToEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendSignInLinkToEmailCallback)completion {} + +- (BOOL)signOut:(NSError *_Nullable *_Nullable)error { + return YES; +} + +- (BOOL)isSignInWithEmailLink:(NSString *)link { + return YES; +} + +- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener: + (FIRAuthStateDidChangeListenerBlock)listener { + return nil; +} + +- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle {} + +- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener: + (FIRIDTokenDidChangeListenerBlock)listener { + return nil; +} + +- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle {} + +- (void)useAppLanguage {} + +- (BOOL)canHandleURL:(nonnull NSURL *)URL { + return NO; +} + +- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type {} + +- (BOOL)canHandleNotification:(NSDictionary *)userInfo { + return NO; +} + +- (BOOL)useUserAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError { + return NO; +} + +- (nullable FIRUser *)getStoredUserForAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError { + return [[FIRUser alloc] init]; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthAPNSTokenType.h b/auth/src/ios/fake/FIRAuthAPNSTokenType.h new file mode 100644 index 0000000000..4f3c9f6a8a --- /dev/null +++ b/auth/src/ios/fake/FIRAuthAPNSTokenType.h @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * @brief The APNs token type for the app. + */ +typedef NS_ENUM(NSInteger, FIRAuthAPNSTokenType) { + + /** Unknown token type. + The actual token type will be detected from the provisioning profile in the app's bundle. + */ + FIRAuthAPNSTokenTypeUnknown, + + /** Sandbox token type. + */ + FIRAuthAPNSTokenTypeSandbox, + + /** Production token type. + */ + FIRAuthAPNSTokenTypeProd, +} NS_SWIFT_NAME(AuthAPNSTokenType); + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthCredential.h b/auth/src/ios/fake/FIRAuthCredential.h new file mode 100644 index 0000000000..c75d201454 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthCredential.h @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthCredential + @brief Represents a credential. + */ +NS_SWIFT_NAME(AuthCredential) +@interface FIRAuthCredential : NSObject + +/** @property provider + @brief Gets the name of the identity provider for the credential. + */ +@property(nonatomic, copy, readonly) NSString *provider; + +/** @fn init + @brief This is an abstract base class. Concrete instances should be created via factory + methods available in the various authentication provider libraries (like the Facebook + provider or the Google provider libraries.) + */ +- (instancetype)init NS_UNAVAILABLE; + +// Only used for testing. +- (instancetype)initWithProvider:(NSString *)provider NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthCredential.mm b/auth/src/ios/fake/FIRAuthCredential.mm new file mode 100644 index 0000000000..f1f603683e --- /dev/null +++ b/auth/src/ios/fake/FIRAuthCredential.mm @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAuthCredential + +- (instancetype)initWithProvider:(NSString *)provider { + self = [super init]; + if (self) { + _provider = [provider copy]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthDataResult.h b/auth/src/ios/fake/FIRAuthDataResult.h new file mode 100644 index 0000000000..42770d66e6 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthDataResult.h @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAdditionalUserInfo; +@class FIRAuthCredential; +@class FIRUser; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthDataResult + @brief Helper object that contains the result of a successful sign-in, link and reauthenticate + action. It contains references to a FIRUser instance and a FIRAdditionalUserInfo instance. + */ +NS_SWIFT_NAME(AuthDataResult) +@interface FIRAuthDataResult : NSObject + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be initialized manually. `FIRAuthDataResult` instance is + returned as part of `FIRAuthDataResultCallback`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @property user + @brief The signed in user. + */ +@property(nonatomic, readonly) FIRUser *user; + +/** @property additionalUserInfo + @brief If available contains the additional IdP specific information about signed in user. + */ +@property(nonatomic, readonly, nullable) FIRAdditionalUserInfo *additionalUserInfo; + +/** @property credential + @brief This property will be non-nil after a successful headful-lite sign-in via + signInWithProvider:UIDelegate:. May be used to obtain the accessToken and/or IDToken + pertaining to a recently signed-in user. + */ +@property(nonatomic, readonly, nullable) FIRAuthCredential *credential; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthDataResult.mm b/auth/src/ios/fake/FIRAuthDataResult.mm new file mode 100644 index 0000000000..fe7c9f81c4 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthDataResult.mm @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuthDataResult.h" + +#import "auth/src/ios/fake/FIRUser.h" +#import "auth/src/ios/fake/FIRAdditionalUserInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAuthDataResult + +- (instancetype)init { + self = [super init]; + if (self) { + _user = [[FIRUser alloc] init]; + _additionalUserInfo = [[FIRAdditionalUserInfo alloc] init]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthErrors.h b/auth/src/ios/fake/FIRAuthErrors.h new file mode 100644 index 0000000000..8874fb6111 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthErrors.h @@ -0,0 +1,358 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthErrors + @remarks Error Codes common to all API Methods: + + + `FIRAuthErrorCodeNetworkError` + + `FIRAuthErrorCodeUserNotFound` + + `FIRAuthErrorCodeUserTokenExpired` + + `FIRAuthErrorCodeTooManyRequests` + + `FIRAuthErrorCodeInvalidAPIKey` + + `FIRAuthErrorCodeAppNotAuthorized` + + `FIRAuthErrorCodeKeychainError` + + `FIRAuthErrorCodeInternalError` + + @remarks Common error codes for `FIRUser` operations: + + + `FIRAuthErrorCodeInvalidUserToken` + + `FIRAuthErrorCodeUserDisabled` + + */ +NS_SWIFT_NAME(AuthErrors) +@interface FIRAuthErrors + +/** + @brief The Firebase Auth error domain. + */ +extern NSString *const FIRAuthErrorDomain NS_SWIFT_NAME(AuthErrorDomain); + +/** + @brief The name of the key for the error short string of an error code. + */ +extern NSString *const FIRAuthErrorUserInfoNameKey NS_SWIFT_NAME(AuthErrorUserInfoNameKey); + +/** + @brief Errors with one of the following three codes: + - `FIRAuthErrorCodeAccountExistsWithDifferentCredential` + - `FIRAuthErrorCodeCredentialAlreadyInUse` + - `FIRAuthErrorCodeEmailAlreadyInUse` + may contain an `NSError.userInfo` dictinary object which contains this key. The value + associated with this key is an NSString of the email address of the account that already + exists. + */ +extern NSString *const FIRAuthErrorUserInfoEmailKey NS_SWIFT_NAME(AuthErrorUserInfoEmailKey); + +/** + @brief The key used to read the updated Auth credential from the userInfo dictionary of the + NSError object returned. This is the updated auth credential the developer should use for + recovery if applicable. + */ +extern NSString *const FIRAuthErrorUserInfoUpdatedCredentialKey + NS_SWIFT_NAME(AuthErrorUserInfoUpdatedCredentialKey); + +/** + @brief Error codes used by Firebase Auth. + */ +typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { + /** Indicates a validation error with the custom token. + */ + FIRAuthErrorCodeInvalidCustomToken = 17000, + + /** Indicates the service account and the API key belong to different projects. + */ + FIRAuthErrorCodeCustomTokenMismatch = 17002, + + /** Indicates the IDP token or requestUri is invalid. + */ + FIRAuthErrorCodeInvalidCredential = 17004, + + /** Indicates the user's account is disabled on the server. + */ + FIRAuthErrorCodeUserDisabled = 17005, + + /** Indicates the administrator disabled sign in with the specified identity provider. + */ + FIRAuthErrorCodeOperationNotAllowed = 17006, + + /** Indicates the email used to attempt a sign up is already in use. + */ + FIRAuthErrorCodeEmailAlreadyInUse = 17007, + + /** Indicates the email is invalid. + */ + FIRAuthErrorCodeInvalidEmail = 17008, + + /** Indicates the user attempted sign in with a wrong password. + */ + FIRAuthErrorCodeWrongPassword = 17009, + + /** Indicates that too many requests were made to a server method. + */ + FIRAuthErrorCodeTooManyRequests = 17010, + + /** Indicates the user account was not found. + */ + FIRAuthErrorCodeUserNotFound = 17011, + + /** Indicates account linking is required. + */ + FIRAuthErrorCodeAccountExistsWithDifferentCredential = 17012, + + /** Indicates the user has attemped to change email or password more than 5 minutes after + signing in. + */ + FIRAuthErrorCodeRequiresRecentLogin = 17014, + + /** Indicates an attempt to link a provider to which the account is already linked. + */ + FIRAuthErrorCodeProviderAlreadyLinked = 17015, + + /** Indicates an attempt to unlink a provider that is not linked. + */ + FIRAuthErrorCodeNoSuchProvider = 17016, + + /** Indicates user's saved auth credential is invalid, the user needs to sign in again. + */ + FIRAuthErrorCodeInvalidUserToken = 17017, + + /** Indicates a network error occurred (such as a timeout, interrupted connection, or + unreachable host). These types of errors are often recoverable with a retry. The + `NSUnderlyingError` field in the `NSError.userInfo` dictionary will contain the error + encountered. + */ + FIRAuthErrorCodeNetworkError = 17020, + + /** Indicates the saved token has expired, for example, the user may have changed account + password on another device. The user needs to sign in again on the device that made this + request. + */ + FIRAuthErrorCodeUserTokenExpired = 17021, + + /** Indicates an invalid API key was supplied in the request. + */ + FIRAuthErrorCodeInvalidAPIKey = 17023, + + /** Indicates that an attempt was made to reauthenticate with a user which is not the current + user. + */ + FIRAuthErrorCodeUserMismatch = 17024, + + /** Indicates an attempt to link with a credential that has already been linked with a + different Firebase account + */ + FIRAuthErrorCodeCredentialAlreadyInUse = 17025, + + /** Indicates an attempt to set a password that is considered too weak. + */ + FIRAuthErrorCodeWeakPassword = 17026, + + /** Indicates the App is not authorized to use Firebase Authentication with the + provided API Key. + */ + FIRAuthErrorCodeAppNotAuthorized = 17028, + + /** Indicates the OOB code is expired. + */ + FIRAuthErrorCodeExpiredActionCode = 17029, + + /** Indicates the OOB code is invalid. + */ + FIRAuthErrorCodeInvalidActionCode = 17030, + + /** Indicates that there are invalid parameters in the payload during a "send password reset + * email" attempt. + */ + FIRAuthErrorCodeInvalidMessagePayload = 17031, + + /** Indicates that the sender email is invalid during a "send password reset email" attempt. + */ + FIRAuthErrorCodeInvalidSender = 17032, + + /** Indicates that the recipient email is invalid. + */ + FIRAuthErrorCodeInvalidRecipientEmail = 17033, + + /** Indicates that an email address was expected but one was not provided. + */ + FIRAuthErrorCodeMissingEmail = 17034, + + // The enum values 17035 is reserved and should NOT be used for new error codes. + + /** Indicates that the iOS bundle ID is missing when a iOS App Store ID is provided. + */ + FIRAuthErrorCodeMissingIosBundleID = 17036, + + /** Indicates that the android package name is missing when the `androidInstallApp` flag is set + to true. + */ + FIRAuthErrorCodeMissingAndroidPackageName = 17037, + + /** Indicates that the domain specified in the continue URL is not whitelisted in the Firebase + console. + */ + FIRAuthErrorCodeUnauthorizedDomain = 17038, + + /** Indicates that the domain specified in the continue URI is not valid. + */ + FIRAuthErrorCodeInvalidContinueURI = 17039, + + /** Indicates that a continue URI was not provided in a request to the backend which requires + one. + */ + FIRAuthErrorCodeMissingContinueURI = 17040, + + /** Indicates that a phone number was not provided in a call to + `verifyPhoneNumber:completion:`. + */ + FIRAuthErrorCodeMissingPhoneNumber = 17041, + + /** Indicates that an invalid phone number was provided in a call to + `verifyPhoneNumber:completion:`. + */ + FIRAuthErrorCodeInvalidPhoneNumber = 17042, + + /** Indicates that the phone auth credential was created with an empty verification code. + */ + FIRAuthErrorCodeMissingVerificationCode = 17043, + + /** Indicates that an invalid verification code was used in the verifyPhoneNumber request. + */ + FIRAuthErrorCodeInvalidVerificationCode = 17044, + + /** Indicates that the phone auth credential was created with an empty verification ID. + */ + FIRAuthErrorCodeMissingVerificationID = 17045, + + /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request. + */ + FIRAuthErrorCodeInvalidVerificationID = 17046, + + /** Indicates that the APNS device token is missing in the verifyClient request. + */ + FIRAuthErrorCodeMissingAppCredential = 17047, + + /** Indicates that an invalid APNS device token was used in the verifyClient request. + */ + FIRAuthErrorCodeInvalidAppCredential = 17048, + + // The enum values between 17048 and 17051 are reserved and should NOT be used for new error + // codes. + + /** Indicates that the SMS code has expired. + */ + FIRAuthErrorCodeSessionExpired = 17051, + + /** Indicates that the quota of SMS messages for a given project has been exceeded. + */ + FIRAuthErrorCodeQuotaExceeded = 17052, + + /** Indicates that the APNs device token could not be obtained. The app may not have set up + remote notification correctly, or may fail to forward the APNs device token to FIRAuth + if app delegate swizzling is disabled. + */ + FIRAuthErrorCodeMissingAppToken = 17053, + + /** Indicates that the app fails to forward remote notification to FIRAuth. + */ + FIRAuthErrorCodeNotificationNotForwarded = 17054, + + /** Indicates that the app could not be verified by Firebase during phone number authentication. + */ + FIRAuthErrorCodeAppNotVerified = 17055, + + /** Indicates that the reCAPTCHA token is not valid. + */ + FIRAuthErrorCodeCaptchaCheckFailed = 17056, + + /** Indicates that an attempt was made to present a new web context while one was already being + presented. + */ + FIRAuthErrorCodeWebContextAlreadyPresented = 17057, + + /** Indicates that the URL presentation was cancelled prematurely by the user. + */ + FIRAuthErrorCodeWebContextCancelled = 17058, + + /** Indicates a general failure during the app verification flow. + */ + FIRAuthErrorCodeAppVerificationUserInteractionFailure = 17059, + + /** Indicates that the clientID used to invoke a web flow is invalid. + */ + FIRAuthErrorCodeInvalidClientID = 17060, + + /** Indicates that a network request within a SFSafariViewController or UIWebview failed. + */ + FIRAuthErrorCodeWebNetworkRequestFailed = 17061, + + /** Indicates that an internal error occurred within a SFSafariViewController or UIWebview. + */ + FIRAuthErrorCodeWebInternalError = 17062, + + /** Indicates a general failure during a web sign-in flow. + */ + FIRAuthErrorCodeWebSignInUserInteractionFailure = 17063, + + /** Indicates that the local player was not authenticated prior to attempting Game Center + signin. + */ + FIRAuthErrorCodeLocalPlayerNotAuthenticated = 17066, + + /** Indicates that a non-null user was expected as an argmument to the operation but a null + user was provided. + */ + FIRAuthErrorCodeNullUser = 17067, + + /** + * Represents the error code for when the given provider id for a web operation is invalid. + */ + FIRAuthErrorCodeInvalidProviderID = 17071, + + /** Indicates that the Firebase Dynamic Link domain used is either not configured or is + unauthorized for the current project. + */ + FIRAuthErrorCodeInvalidDynamicLinkDomain = 17074, + + /** Indicates that the GameKit framework is not linked prior to attempting Game Center signin. + */ + FIRAuthErrorCodeGameKitNotLinked = 17076, + + /** Indicates an error for when the client identifier is missing. + */ + FIRAuthErrorCodeMissingClientIdentifier = 17993, + + /** Indicates an error occurred while attempting to access the keychain. + */ + FIRAuthErrorCodeKeychainError = 17995, + + /** Indicates an internal error occurred. + */ + FIRAuthErrorCodeInternalError = 17999, + + /** Raised when a JWT fails to parse correctly. May be accompanied by an underlying error + describing which step of the JWT parsing process failed. + */ + FIRAuthErrorCodeMalformedJWT = 18000, +} NS_SWIFT_NAME(AuthErrorCode); + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthSettings.h b/auth/src/ios/fake/FIRAuthSettings.h new file mode 100644 index 0000000000..4ac7ce8762 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthSettings.h @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthSettings + @brief Determines settings related to an auth object. + */ +NS_SWIFT_NAME(AuthSettings) +@interface FIRAuthSettings : NSObject + +/** @property appVerificationDisabledForTesting + @brief Flag to determine whether app verification should be disabled for testing or not. + */ +@property(nonatomic, assign, getter=isAppVerificationDisabledForTesting) BOOL + appVerificationDisabledForTesting; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthTokenResult.h b/auth/src/ios/fake/FIRAuthTokenResult.h new file mode 100644 index 0000000000..515aa60d2c --- /dev/null +++ b/auth/src/ios/fake/FIRAuthTokenResult.h @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthTokenResult + @brief A data class containing the ID token JWT string and other properties associated with the + token including the decoded payload claims. + */ +NS_SWIFT_NAME(AuthTokenResult) +@interface FIRAuthTokenResult : NSObject + +/** @property token + @brief Stores the JWT string of the ID token. + */ +@property(nonatomic, readonly) NSString *token; + +/** @property expirationDate + @brief Stores the ID token's expiration date. + */ +@property(nonatomic, readonly) NSDate *expirationDate; + +/** @property authDate + @brief Stores the ID token's authentication date. + @remarks This is the date the user was signed in and NOT the date the token was refreshed. + */ +@property(nonatomic, readonly) NSDate *authDate; + +/** @property issuedAtDate + @brief Stores the date that the ID token was issued. + @remarks This is the date last refreshed and NOT the last authentication date. + */ +@property(nonatomic, readonly) NSDate *issuedAtDate; + +/** @property signInProvider + @brief Stores sign-in provider through which the token was obtained. + @remarks This does not necessarily map to provider IDs. + */ +@property(nonatomic, readonly) NSString *signInProvider; + +/** @property claims + @brief Stores the entire payload of claims found on the ID token. This includes the standard + reserved claims as well as custom claims set by the developer via the Admin SDK. + */ +@property(nonatomic, readonly) NSDictionary *claims; + + + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthUIDelegate.h b/auth/src/ios/fake/FIRAuthUIDelegate.h new file mode 100644 index 0000000000..9df4f6e407 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthUIDelegate.h @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class UIViewController; + +NS_ASSUME_NONNULL_BEGIN + +/** @protocol FIRAuthUIDelegate + @brief A protocol to handle user interface interactions for Firebase Auth. + */ +NS_SWIFT_NAME(AuthUIDelegate) +@protocol FIRAuthUIDelegate + +/** @fn presentViewController:animated:completion: + @brief If implemented, this method will be invoked when Firebase Auth needs to display a view + controller. + @param viewControllerToPresent The view controller to be presented. + @param flag Decides whether the view controller presentation should be animated or not. + @param completion The block to execute after the presentation finishes. This block has no return + value and takes no parameters. +*/ +- (void)presentViewController:(UIViewController *)viewControllerToPresent + animated:(BOOL)flag + completion:(void (^ _Nullable)(void))completion; + +/** @fn dismissViewControllerAnimated:completion: + @brief If implemented, this method will be invoked when Firebase Auth needs to display a view + controller. + @param flag Decides whether removing the view controller should be animated or not. + @param completion The block to execute after the presentation finishes. This block has no return + value and takes no parameters. +*/ +- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion + NS_SWIFT_NAME(dismiss(animated:completion:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIREmailAuthProvider.h b/auth/src/ios/fake/FIREmailAuthProvider.h new file mode 100644 index 0000000000..aac0bf0a0f --- /dev/null +++ b/auth/src/ios/fake/FIREmailAuthProvider.h @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the email & password identity provider. + */ +extern NSString *const FIREmailAuthProviderID NS_SWIFT_NAME(EmailAuthProviderID); + +/** + @brief A string constant identifying the email-link sign-in method. + */ +extern NSString *const FIREmailLinkAuthSignInMethod NS_SWIFT_NAME(EmailLinkAuthSignInMethod); + +/** + @brief A string constant identifying the email & password sign-in method. + */ +extern NSString *const FIREmailPasswordAuthSignInMethod + NS_SWIFT_NAME(EmailPasswordAuthSignInMethod); + +/** @class FIREmailAuthProvider + @brief A concrete implementation of `FIRAuthProvider` for Email & Password Sign In. + */ +NS_SWIFT_NAME(EmailAuthProvider) +@interface FIREmailAuthProvider : NSObject + +/** @fn credentialWithEmail:password: + @brief Creates an `FIRAuthCredential` for an email & password sign in. + + @param email The user's email address. + @param password The user's password. + @return A FIRAuthCredential containing the email & password credential. + */ ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password; + +/** @fn credentialWithEmail:Link: + @brief Creates an `FIRAuthCredential` for an email & link sign in. + + @param email The user's email address. + @param link The email sign-in link. + @return A FIRAuthCredential containing the email & link credential. + */ ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)link; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIREmailAuthProvider.mm b/auth/src/ios/fake/FIREmailAuthProvider.mm new file mode 100644 index 0000000000..52def576c0 --- /dev/null +++ b/auth/src/ios/fake/FIREmailAuthProvider.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIREmailAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIREmailAuthProvider + ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password { + return [[FIRAuthCredential alloc] initWithProvider:@"password"]; +} + ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)link { + return [[FIRAuthCredential alloc] initWithProvider:@"link"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRFacebookAuthProvider.h b/auth/src/ios/fake/FIRFacebookAuthProvider.h new file mode 100644 index 0000000000..75efe13f4a --- /dev/null +++ b/auth/src/ios/fake/FIRFacebookAuthProvider.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Facebook identity provider. + */ +extern NSString *const FIRFacebookAuthProviderID NS_SWIFT_NAME(FacebookAuthProviderID); + +/** + @brief A string constant identifying the Facebook sign-in method. + */ +extern NSString *const _Nonnull FIRFacebookAuthSignInMethod NS_SWIFT_NAME(FacebookAuthSignInMethod); + +/** @class FIRFacebookAuthProvider + @brief Utility class for constructing Facebook credentials. + */ +NS_SWIFT_NAME(FacebookAuthProvider) +@interface FIRFacebookAuthProvider : NSObject + +/** @fn credentialWithAccessToken: + @brief Creates an `FIRAuthCredential` for a Facebook sign in. + + @param accessToken The Access Token from Facebook. + @return A FIRAuthCredential containing the Facebook credentials. + */ ++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken; + +/** @fn init + @brief This class should not be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRFacebookAuthProvider.mm b/auth/src/ios/fake/FIRFacebookAuthProvider.mm new file mode 100644 index 0000000000..21c7e39ebe --- /dev/null +++ b/auth/src/ios/fake/FIRFacebookAuthProvider.mm @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuthCredential.h" +#import "auth/src/ios/fake/FIRFacebookAuthProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRFacebookAuthProvider + ++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken { + return [[FIRAuthCredential alloc] initWithProvider:@"facebook.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRFederatedAuthProvider.h b/auth/src/ios/fake/FIRFederatedAuthProvider.h new file mode 100644 index 0000000000..51190e28cd --- /dev/null +++ b/auth/src/ios/fake/FIRFederatedAuthProvider.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#if TARGET_OS_IOS +#import "FIRAuthUIDelegate.h" +#endif // TARGET_OS_IOS + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(FederatedAuthProvider) +@protocol FIRFederatedAuthProvider + +/** @typedef FIRAuthCredentialCallback + @brief The type of block invoked when obtaining an auth credential. + @param credential The credential obtained. + @param error The error that occurred if any. + */ +typedef void(^FIRAuthCredentialCallback)(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthCredentialCallback); + +#if TARGET_OS_IOS +/** @fn getCredentialWithUIDelegate:completion: + @brief Used to obtain an auth credential via a mobile web flow. + @param UIDelegate An optional UI delegate used to presenet the mobile web flow. + @param completion Optionally; a block which is invoked asynchronously on the main thread when + the mobile web flow is completed. + */ +- (void)getCredentialWithUIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthCredentialCallback)completion; +#endif // TARGET_OS_IOS + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGameCenterAuthProvider.h b/auth/src/ios/fake/FIRGameCenterAuthProvider.h new file mode 100644 index 0000000000..5e59404ada --- /dev/null +++ b/auth/src/ios/fake/FIRGameCenterAuthProvider.h @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Game Center identity provider. + */ +extern NSString *const FIRGameCenterAuthProviderID NS_SWIFT_NAME(GameCenterAuthProviderID); + +/** + @brief A string constant identifying the Game Center sign-in method. + */ +extern NSString *const _Nonnull FIRGameCenterAuthSignInMethod +NS_SWIFT_NAME(GameCenterAuthSignInMethod); + +/** @typedef FIRGameCenterCredentialCallback + @brief The type of block invoked when the Game Center credential code has finished. + @param credential On success, the credential will be provided, nil otherwise. + @param error On error, the error that occurred, nil otherwise. + */ +typedef void (^FIRGameCenterCredentialCallback)(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) +NS_SWIFT_NAME(GameCenterCredentialCallback); + +/** @class FIRGameCenterAuthProvider + @brief A concrete implementation of @c FIRAuthProvider for Game Center Sign In. + */ +NS_SWIFT_NAME(GameCenterAuthProvider) +@interface FIRGameCenterAuthProvider : NSObject + +/** @fn getCredentialWithCompletion: + @brief Creates a @c FIRAuthCredential for a Game Center sign in. + */ ++ (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion +NS_SWIFT_NAME(getCredential(completion:)); + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGameCenterAuthProvider.mm b/auth/src/ios/fake/FIRGameCenterAuthProvider.mm new file mode 100644 index 0000000000..854b925900 --- /dev/null +++ b/auth/src/ios/fake/FIRGameCenterAuthProvider.mm @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRGameCenterAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +@implementation FIRGameCenterAuthProvider + ++ (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion { + completion([[FIRAuthCredential alloc] initWithProvider:@"gc.apple.com"], nil); +} + +@end diff --git a/auth/src/ios/fake/FIRGitHubAuthProvider.h b/auth/src/ios/fake/FIRGitHubAuthProvider.h new file mode 100644 index 0000000000..0610427a44 --- /dev/null +++ b/auth/src/ios/fake/FIRGitHubAuthProvider.h @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the GitHub identity provider. + */ +extern NSString *const FIRGitHubAuthProviderID NS_SWIFT_NAME(GitHubAuthProviderID); + +/** + @brief A string constant identifying the GitHub sign-in method. + */ +extern NSString *const _Nonnull FIRGitHubAuthSignInMethod NS_SWIFT_NAME(GitHubAuthSignInMethod); + + +/** @class FIRGitHubAuthProvider + @brief Utility class for constructing GitHub credentials. + */ +NS_SWIFT_NAME(GitHubAuthProvider) +@interface FIRGitHubAuthProvider : NSObject + +/** @fn credentialWithToken: + @brief Creates an `FIRAuthCredential` for a GitHub sign in. + + @param token The GitHub OAuth access token. + @return A FIRAuthCredential containing the GitHub credential. + */ ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGitHubAuthProvider.mm b/auth/src/ios/fake/FIRGitHubAuthProvider.mm new file mode 100644 index 0000000000..6abb95e40e --- /dev/null +++ b/auth/src/ios/fake/FIRGitHubAuthProvider.mm @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRGitHubAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGitHubAuthProvider + ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token { + return [[FIRAuthCredential alloc] initWithProvider:@"github.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGoogleAuthProvider.h b/auth/src/ios/fake/FIRGoogleAuthProvider.h new file mode 100644 index 0000000000..7d6fa226e5 --- /dev/null +++ b/auth/src/ios/fake/FIRGoogleAuthProvider.h @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Google identity provider. + */ +extern NSString *const FIRGoogleAuthProviderID NS_SWIFT_NAME(GoogleAuthProviderID); + +/** + @brief A string constant identifying the Google sign-in method. + */ +extern NSString *const _Nonnull FIRGoogleAuthSignInMethod NS_SWIFT_NAME(GoogleAuthSignInMethod); + +/** @class FIRGoogleAuthProvider + @brief Utility class for constructing Google Sign In credentials. + */ +NS_SWIFT_NAME(GoogleAuthProvider) +@interface FIRGoogleAuthProvider : NSObject + +/** @fn credentialWithIDToken:accessToken: + @brief Creates an `FIRAuthCredential` for a Google sign in. + + @param IDToken The ID Token from Google. + @param accessToken The Access Token from Google. + @return A FIRAuthCredential containing the Google credentials. + */ ++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken + accessToken:(NSString *)accessToken; + +/** @fn init + @brief This class should not be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGoogleAuthProvider.mm b/auth/src/ios/fake/FIRGoogleAuthProvider.mm new file mode 100644 index 0000000000..7e374c063e --- /dev/null +++ b/auth/src/ios/fake/FIRGoogleAuthProvider.mm @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRGoogleAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGoogleAuthProvider + ++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken + accessToken:(NSString *)accessToken { + return [[FIRAuthCredential alloc] initWithProvider:@"google.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthCredential.h b/auth/src/ios/fake/FIROAuthCredential.h new file mode 100644 index 0000000000..5e54198140 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthCredential.h @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIROAuthCredential + @brief Internal implementation of FIRAuthCredential for generic credentials. + */ +NS_SWIFT_NAME(OAuthCredential) +@interface FIROAuthCredential : FIRAuthCredential + +/** @property IDToken + @brief The ID Token associated with this credential. + */ +@property(nonatomic, readonly, nullable) NSString *IDToken; + +/** @property accessToken + @brief The access token associated with this credential. + */ +@property(nonatomic, readonly, nullable) NSString *accessToken; + +/** @property secret + @brief The secret associated with this credential. This will be nil for OAuth 2.0 providers. + @detail OAuthCredential already exposes a providerId getter. This will help the developer + determine whether an access token/secret pair is needed. + */ +@property(nonatomic, readonly, nullable) NSString *secret; + +#if !defined(FIREBASE_AUTH_TESTING) +/** @fn init + @brief This class is not supposed to be instantiated directly. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // !defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthCredential.mm b/auth/src/ios/fake/FIROAuthCredential.mm new file mode 100644 index 0000000000..81852363b8 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthCredential.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIROAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIROAuthCredential + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder {} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithProvider:@"oauth"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthProvider.h b/auth/src/ios/fake/FIROAuthProvider.h new file mode 100644 index 0000000000..a46eb2ccf0 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthProvider.h @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRFederatedAuthProvider.h" + +@class FIRAuth; +@class FIROAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIROAuthProvider + @brief A concrete implementation of `FIRAuthProvider` for generic OAuth Providers. + */ +NS_SWIFT_NAME(OAuthProvider) +@interface FIROAuthProvider : NSObject + +/** @property scopes + @brief Array used to configure the OAuth scopes. + */ +@property(nonatomic, copy, nullable) NSArray *scopes; + +/** @property customParameters + @brief Dictionary used to configure the OAuth custom parameters. + */ +@property(nonatomic, copy, nullable) NSDictionary *customParameters; + +/** @property providerID + @brief The provider ID indicating the specific OAuth provider this OAuthProvider instance + represents. + */ +@property(nonatomic, copy, readonly) NSString *providerID; + +/** @fn providerWithProviderID: + @param providerID The provider ID of the IDP for which this auth provider instance will be + configured. + @return An instance of FIROAuthProvider corresponding to the specified provider ID. + */ ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID; + +/** @fn providerWithProviderID:auth: + @param providerID The provider ID of the IDP for which this auth provider instance will be + configured. + @param auth The auth instance to be associated with the FIROAuthProvider instance. + @return An instance of FIROAuthProvider corresponding to the specified provider ID. + */ ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID auth:(FIRAuth *)auth; + +/** @fn credentialWithProviderID:IDToken:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID, ID + token and access token. + + @param providerID The provider ID associated with the Auth credential being created. + @param IDToken The IDToken associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created, if + available. + @return A FIRAuthCredential for the specified provider ID, ID token and access token. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken; + +/** @fn credentialWithProviderID:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID using + an ID token. + + @param providerID The provider ID associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created + @return A FIRAuthCredential. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + accessToken:(NSString *)accessToken; + +/** @fn credentialWithProviderID:IDToken:rawNonce:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID, ID + token, raw nonce and access token. + @param providerID The provider ID associated with the Auth credential being created. + @param IDToken The IDToken associated with the Auth credential being created. + @param rawNonce The raw nonce associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created, if + available. + @return A FIRAuthCredential for the specified provider ID, ID token and access token. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + rawNonce:(nullable NSString *)rawNonce + accessToken:(nullable NSString *)accessToken; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +// Exposed for testing. +- (instancetype)initWithProviderID:(NSString *)providerID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthProvider.mm b/auth/src/ios/fake/FIROAuthProvider.mm new file mode 100644 index 0000000000..44862f8089 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthProvider.mm @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIROAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" +#import "auth/src/ios/fake/FIROAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIROAuthProvider + +- (void)getCredentialWithUIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthCredentialCallback)completion {} + ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID { + return [[FIROAuthProvider alloc] initWithProviderID:providerID]; +} + ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID + auth:(FIRAuth *)auth { + return [[FIROAuthProvider alloc] initWithProviderID:providerID]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken { + return [[FIROAuthCredential alloc] initWithProvider:providerID]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + accessToken:(NSString *)accessToken { + return [[FIROAuthCredential alloc] initWithProvider:providerID]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + rawNonce:(nullable NSString *)rawNonce + accessToken:(nullable NSString *)accessToken { + return [[FIROAuthCredential alloc] initWithProvider:providerID]; +} + +#pragma mark - Internal Methods + +- (instancetype)initWithProviderID:(NSString *)providerID { + self = [super init]; + if (self) { + _providerID = providerID; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthCredential.h b/auth/src/ios/fake/FIRPhoneAuthCredential.h new file mode 100644 index 0000000000..8badab6a25 --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthCredential.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRPhoneAuthCredential + @brief Implementation of FIRAuthCredential for Phone Auth credentials. + */ +NS_SWIFT_NAME(PhoneAuthCredential) +@interface FIRPhoneAuthCredential : FIRAuthCredential + +#if !defined(FIREBASE_AUTH_TESTING) +/** @fn init + @brief This class is not supposed to be instantiated directly. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // !defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthCredential.mm b/auth/src/ios/fake/FIRPhoneAuthCredential.mm new file mode 100644 index 0000000000..e64933fead --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthCredential.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRPhoneAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRPhoneAuthCredential + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder {} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithProvider:@"phone"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthProvider.h b/auth/src/ios/fake/FIRPhoneAuthProvider.h new file mode 100644 index 0000000000..805d0065f2 --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthProvider.h @@ -0,0 +1,109 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuth; +@class FIRPhoneAuthCredential; +@protocol FIRAuthUIDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** @var FIRPhoneAuthProviderID + @brief A string constant identifying the phone identity provider. + */ +extern NSString *const FIRPhoneAuthProviderID NS_SWIFT_NAME(PhoneAuthProviderID); + +/** @var FIRPhoneAuthProviderID + @brief A string constant identifying the phone sign-in method. + */ +extern NSString *const _Nonnull FIRPhoneAuthSignInMethod NS_SWIFT_NAME(PhoneAuthSignInMethod); + +/** @typedef FIRVerificationResultCallback + @brief The type of block invoked when a request to send a verification code has finished. + + @param verificationID On success, the verification ID provided, nil otherwise. + @param error On error, the error that occurred, nil otherwise. + */ +typedef void (^FIRVerificationResultCallback) + (NSString *_Nullable verificationID, NSError *_Nullable error) + NS_SWIFT_NAME(VerificationResultCallback); + +/** @class FIRPhoneAuthProvider + @brief A concrete implementation of `FIRAuthProvider` for phone auth providers. + */ +NS_SWIFT_NAME(PhoneAuthProvider) +@interface FIRPhoneAuthProvider : NSObject + +/** @fn provider + @brief Returns an instance of `FIRPhoneAuthProvider` for the default `FIRAuth` object. + */ ++ (instancetype)provider NS_SWIFT_NAME(provider()); + +/** @fn providerWithAuth: + @brief Returns an instance of `FIRPhoneAuthProvider` for the provided `FIRAuth` object. + + @param auth The auth object to associate with the phone auth provider instance. + */ ++ (instancetype)providerWithAuth:(FIRAuth *)auth NS_SWIFT_NAME(provider(auth:)); + +/** @fn verifyPhoneNumber:UIDelegate:completion: + @brief Starts the phone number authentication flow by sending a verification code to the + specified phone number. + @param phoneNumber The phone number to be verified. + @param UIDelegate An object used to present the SFSafariViewController. The object is retained + by this method until the completion block is executed. + @param completion The callback to be invoked when the verification flow is finished. + @remarks Possible error codes: + + + `FIRAuthErrorCodeCaptchaCheckFailed` - Indicates that the reCAPTCHA token obtained by + the Firebase Auth is invalid or has expired. + + `FIRAuthErrorCodeQuotaExceeded` - Indicates that the phone verification quota for this + project has been exceeded. + + `FIRAuthErrorCodeInvalidPhoneNumber` - Indicates that the phone number provided is + invalid. + + `FIRAuthErrorCodeMissingPhoneNumber` - Indicates that a phone number was not provided. + */ +- (void)verifyPhoneNumber:(NSString *)phoneNumber + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRVerificationResultCallback)completion; + +/** @fn credentialWithVerificationID:verificationCode: + @brief Creates an `FIRAuthCredential` for the phone number provider identified by the + verification ID and verification code. + + @param verificationID The verification ID obtained from invoking + verifyPhoneNumber:completion: + @param verificationCode The verification code obtained from the user. + @return The corresponding phone auth credential for the verification ID and verification code + provided. + */ +- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID + verificationCode:(NSString *)verificationCode; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief Please use the `provider` or `providerWithAuth:` methods to obtain an instance of + `FIRPhoneAuthProvider`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthProvider.mm b/auth/src/ios/fake/FIRPhoneAuthProvider.mm new file mode 100644 index 0000000000..2e1d735241 --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthProvider.mm @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRPhoneAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" +#import "auth/src/ios/fake/FIRPhoneAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRPhoneAuthProvider + +- (instancetype)init { + return [super init]; +} + ++ (instancetype)provider { + return [[FIRPhoneAuthProvider alloc] init]; +} + ++ (instancetype)providerWithAuth:(FIRAuth *)auth { + return [FIRPhoneAuthProvider provider]; +} + +- (void)verifyPhoneNumber:(NSString *)phoneNumber + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRVerificationResultCallback)completion {} + +- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID + verificationCode:(NSString *)verificationCode { + return [[FIRPhoneAuthCredential alloc] initWithProvider:@"phone"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRTwitterAuthProvider.h b/auth/src/ios/fake/FIRTwitterAuthProvider.h new file mode 100644 index 0000000000..0f1b28d737 --- /dev/null +++ b/auth/src/ios/fake/FIRTwitterAuthProvider.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Twitter identity provider. + */ +extern NSString *const FIRTwitterAuthProviderID NS_SWIFT_NAME(TwitterAuthProviderID); +/** + @brief A string constant identifying the Twitter sign-in method. + */ +extern NSString *const _Nonnull FIRTwitterAuthSignInMethod NS_SWIFT_NAME(TwitterAuthSignInMethod); + +/** @class FIRTwitterAuthProvider + @brief Utility class for constructing Twitter credentials. + */ +NS_SWIFT_NAME(TwitterAuthProvider) +@interface FIRTwitterAuthProvider : NSObject + +/** @fn credentialWithToken:secret: + @brief Creates an `FIRAuthCredential` for a Twitter sign in. + + @param token The Twitter OAuth token. + @param secret The Twitter OAuth secret. + @return A FIRAuthCredential containing the Twitter credential. + */ ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRTwitterAuthProvider.mm b/auth/src/ios/fake/FIRTwitterAuthProvider.mm new file mode 100644 index 0000000000..515573e68d --- /dev/null +++ b/auth/src/ios/fake/FIRTwitterAuthProvider.mm @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRTwitterAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRTwitterAuthProvider + ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret { + return [[FIRAuthCredential alloc] initWithProvider:@"twitter.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUser.h b/auth/src/ios/fake/FIRUser.h new file mode 100644 index 0000000000..f65108749b --- /dev/null +++ b/auth/src/ios/fake/FIRUser.h @@ -0,0 +1,507 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRAuth.h" +#import "FIRAuthDataResult.h" +#import "FIRUserInfo.h" + +@class FIRAuthTokenResult; +@class FIRPhoneAuthCredential; +@class FIRUserProfileChangeRequest; +@class FIRUserMetadata; + +namespace firebase { +namespace testing { +namespace cppsdk { +class CallbackTickerManager; +} // namespace cppsdk +} // namespace testing +} // namespace firebase + +NS_ASSUME_NONNULL_BEGIN + +/** @typedef FIRAuthTokenCallback + @brief The type of block called when a token is ready for use. + @see FIRUser.getIDTokenWithCompletion: + @see FIRUser.getIDTokenForcingRefresh:withCompletion: + + @param token Optionally; an access token if the request was successful. + @param error Optionally; the error which occurred - or nil if the request was successful. + + @remarks One of: `token` or `error` will always be non-nil. + */ +typedef void (^FIRAuthTokenCallback)(NSString *_Nullable token, NSError *_Nullable error) + NS_SWIFT_NAME(AuthTokenCallback); + +/** @typedef FIRAuthTokenResultCallback + @brief The type of block called when a token is ready for use. + @see FIRUser.getIDTokenResultWithCompletion: + @see FIRUser.getIDTokenResultForcingRefresh:withCompletion: + + @param tokenResult Optionally; an object containing the raw access token string as well as other + useful data pertaining to the token. + @param error Optionally; the error which occurred - or nil if the request was successful. + + @remarks One of: `token` or `error` will always be non-nil. + */ +typedef void (^FIRAuthTokenResultCallback)(FIRAuthTokenResult *_Nullable tokenResult, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthTokenResultCallback); + +/** @typedef FIRUserProfileChangeCallback + @brief The type of block called when a user profile change has finished. + + @param error Optionally; the error which occurred - or nil if the request was successful. + */ +typedef void (^FIRUserProfileChangeCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(UserProfileChangeCallback); + +/** @typedef FIRSendEmailVerificationCallback + @brief The type of block called when a request to send an email verification has finished. + + @param error Optionally; the error which occurred - or nil if the request was successful. + */ +typedef void (^FIRSendEmailVerificationCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendEmailVerificationCallback); + +/** @class FIRUser + @brief Represents a user. Firebase Auth does not attempt to validate users + when loading them from the keychain. Invalidated users (such as those + whose passwords have been changed on another client) are automatically + logged out when an auth-dependent operation is attempted or when the + ID token is automatically refreshed. + @remarks This class is thread-safe. + */ +NS_SWIFT_NAME(User) +@interface FIRUser : NSObject + +/** @property anonymous + @brief Indicates the user represents an anonymous user. + */ +@property(nonatomic, readonly, getter=isAnonymous) BOOL anonymous; + +/** @property emailVerified + @brief Indicates the email address associated with this user has been verified. + */ +@property(nonatomic, readonly, getter=isEmailVerified) BOOL emailVerified; + +/** @property refreshToken + @brief A refresh token; useful for obtaining new access tokens independently. + @remarks This property should only be used for advanced scenarios, and is not typically needed. + */ +@property(nonatomic, readonly, nullable) NSString *refreshToken; + +/** @property providerData + @brief Profile data for each identity provider, if any. + @remarks This data is cached on sign-in and updated when linking or unlinking. + */ +@property(nonatomic, readonly, nonnull) NSArray> *providerData; + +/** @property metadata + @brief Metadata associated with the Firebase user in question. + */ +@property(nonatomic, readonly, nonnull) FIRUserMetadata *metadata; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be instantiated. + @remarks To retrieve the current user, use `FIRAuth.currentUser`. To sign a user + in or out, use the methods on `FIRAuth`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @fn updateEmail:completion: + @brief Updates the email address for the user. On success, the cached user profile data is + updated. + @remarks May fail if there is already an account with this email address that was created using + email and password authentication. + + @param email The email address for the user. + @param completion Optionally; the block invoked when the user profile change has finished. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another + account. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security + sensitive operation that requires a recent login from the user. This error indicates + the user has not signed in recently enough. To resolve, reauthenticate the user by + invoking reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion + NS_SWIFT_NAME(updateEmail(to:completion:)); + +/** @fn updatePassword:completion: + @brief Updates the password for the user. On success, the cached user profile data is updated. + + @param password The new password for the user. + @param completion Optionally; the block invoked when the user profile change has finished. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled + sign in with the specified identity provider. + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security + sensitive operation that requires a recent login from the user. This error indicates + the user has not signed in recently enough. To resolve, reauthenticate the user by + invoking reauthenticateWithCredential:completion: on FIRUser. + + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo + dictionary object will contain more detailed explanation that can be shown to the user. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)updatePassword:(NSString *)password + completion:(nullable FIRUserProfileChangeCallback)completion + NS_SWIFT_NAME(updatePassword(to:completion:)); + +#if TARGET_OS_IOS +/** @fn updatePhoneNumberCredential:completion: + @brief Updates the phone number for the user. On success, the cached user profile data is + updated. + + @param phoneNumberCredential The new phone number credential corresponding to the phone number + to be added to the Firebase account, if a phone number is already linked to the account this + new phone number will replace it. + @param completion Optionally; the block invoked when the user profile change has finished. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security + sensitive operation that requires a recent login from the user. This error indicates + the user has not signed in recently enough. To resolve, reauthenticate the user by + invoking reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneNumberCredential + completion:(nullable FIRUserProfileChangeCallback)completion; +#endif + +/** @fn profileChangeRequest + @brief Creates an object which may be used to change the user's profile data. + + @remarks Set the properties of the returned object, then call + `FIRUserProfileChangeRequest.commitChangesWithCallback:` to perform the updates atomically. + + @return An object which may be used to change the user's profile data atomically. + */ +- (FIRUserProfileChangeRequest *)profileChangeRequest NS_SWIFT_NAME(createProfileChangeRequest()); + +/** @fn reloadWithCompletion: + @brief Reloads the user's profile data from the server. + + @param completion Optionally; the block invoked when the reload has finished. Invoked + asynchronously on the main thread in the future. + + @remarks May fail with a `FIRAuthErrorCodeRequiresRecentLogin` error code. In this case + you should call `FIRUser.reauthenticateWithCredential:completion:` before re-invoking + `FIRUser.updateEmail:completion:`. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion; + +/** @fn reauthenticateWithCredential:completion: + @brief Renews the user's authentication tokens by validating a fresh set of credentials supplied + by the user and returns additional identity provider data. + + @param credential A user-supplied credential, which will be validated by the server. This can be + a successful third-party identity provider sign-in, or an email address and password. + @param completion Optionally; the block invoked when the re-authentication operation has + finished. Invoked asynchronously on the main thread in the future. + + @remarks If the user associated with the supplied credential is different from the current user, + or if the validation of the supplied credentials fails; an error is returned and the current + user remains signed in. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + This could happen if it has expired or it is malformed. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that accounts with the + identity provider represented by the credential are not enabled. Enable them in the + Auth section of the Firebase console. + + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential + (e.g. the email in a Facebook access token) is already in use by an existing account, + that cannot be authenticated with this method. Call fetchProvidersForEmail for + this user’s email and then prompt them to sign in with any of the sign-in providers + returned. This error will only be thrown if the "One account per email address" + setting is enabled in the Firebase console, under Auth settings. Please note that the + error code raised in this specific situation may not be the same on Web and Android. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with + an incorrect password, if credential is of the type EmailPasswordAuthCredential. + + `FIRAuthErrorCodeUserMismatch` - Indicates that an attempt was made to + reauthenticate with a user which is not the current user. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn reauthenticateAndRetrieveDataWithCredential:completion: + @brief Please use linkWithCredential:completion: for Objective-C + or link(withCredential:completion:) for Swift instead. + */ +- (void)reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion +DEPRECATED_MSG_ATTRIBUTE( "Please use reauthenticateWithCredential:completion: for" + " Objective-C or reauthenticate(withCredential:completion:)" + " for Swift instead."); + +/** @fn getIDTokenResultWithCompletion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenResultWithCompletion:(nullable FIRAuthTokenResultCallback)completion + NS_SWIFT_NAME(getIDTokenResult(completion:)); + +/** @fn getIDTokenResultForcingRefresh:completion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason + other than an expiration. + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks The authentication token will be refreshed (by making a network request) if it has + expired, or if `forceRefresh` is YES. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenResultForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenResultCallback)completion + NS_SWIFT_NAME(getIDTokenResult(forcingRefresh:completion:)); + +/** @fn getIDTokenWithCompletion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion + NS_SWIFT_NAME(getIDToken(completion:)); + +/** @fn getIDTokenForcingRefresh:completion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason + other than an expiration. + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks The authentication token will be refreshed (by making a network request) if it has + expired, or if `forceRefresh` is YES. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenCallback)completion; + +/** @fn linkAndRetrieveDataWithCredential:completion: + @brief Please use linkWithCredential:completion: for Objective-C + or link(withCredential:completion:) for Swift instead. + */ +- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion +DEPRECATED_MSG_ATTRIBUTE("Please use linkWithCredential:completion: for Objective-C " + "or link(withCredential:completion:) for Swift instead."); + +/** @fn linkWithCredential:completion: + @brief Associates a user account from a third-party identity provider with this user and + returns additional identity provider data. + + @param credential The credential for the identity provider. + @param completion Optionally; the block invoked when the unlinking is complete, or fails. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a + type already linked to this account. + + `FIRAuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a + credential that has already been linked with a different Firebase account. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity + provider represented by the credential are not enabled. Enable them in the Auth section + of the Firebase console. + + @remarks This method may also return error codes associated with updateEmail:completion: and + updatePassword:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)linkWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn unlinkFromProvider:completion: + @brief Disassociates a user account from a third-party identity provider with this user. + + @param provider The provider ID of the provider to unlink. + @param completion Optionally; the block invoked when the unlinking is complete, or fails. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider + that is not linked to the account. + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + operation that requires a recent login from the user. This error indicates the user + has not signed in recently enough. To resolve, reauthenticate the user by invoking + reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)unlinkFromProvider:(NSString *)provider + completion:(nullable FIRAuthResultCallback)completion; + +/** @fn sendEmailVerificationWithCompletion: + @brief Initiates email verification for the user. + + @param completion Optionally; the block invoked when the request to send an email verification + is complete, or fails. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeUserNotFound` - Indicates the user account was not found. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion; + +/** @fn sendEmailVerificationWithActionCodeSettings:completion: + @brief Initiates email verification for the user. + + @param actionCodeSettings An `FIRActionCodeSettings` object containing settings related to + handling action codes. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeUserNotFound` - Indicates the user account was not found. + + `FIRAuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when + a iOS App Store ID is provided. + + `FIRAuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name + is missing when the `androidInstallApp` flag is set to true. + + `FIRAuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the + continue URL is not whitelisted in the Firebase console. + + `FIRAuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the + continue URI is not valid. + */ +- (void)sendEmailVerificationWithActionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendEmailVerificationCallback) + completion; + +/** @fn deleteWithCompletion: + @brief Deletes the user account (also signs out the user, if this was the current user). + + @param completion Optionally; the block invoked when the request to delete the account is + complete, or fails. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + operation that requires a recent login from the user. This error indicates the user + has not signed in recently enough. To resolve, reauthenticate the user by invoking + reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + + */ +- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion; + +@end + +/** @class FIRUserProfileChangeRequest + @brief Represents an object capable of updating a user's profile data. + @remarks Properties are marked as being part of a profile update when they are set. Setting a + property value to nil is not the same as leaving the property unassigned. + */ +NS_SWIFT_NAME(UserProfileChangeRequest) +@interface FIRUserProfileChangeRequest : NSObject + +/** @fn init + @brief Please use `FIRUser.profileChangeRequest` + */ +- (instancetype)init NS_UNAVAILABLE; + +// Only used for testing. +- (instancetype) + initWithCallbackManager:(firebase::testing::cppsdk::CallbackTickerManager *)callbackManager + NS_DESIGNATED_INITIALIZER; + +/** @property displayName + @brief The user's display name. + @remarks It is an error to set this property after calling + `FIRUserProfileChangeRequest.commitChangesWithCallback:` + */ +@property(nonatomic, copy, nullable) NSString *displayName; + +/** @property photoURL + @brief The user's photo URL. + @remarks It is an error to set this property after calling + `FIRUserProfileChangeRequest.commitChangesWithCallback:` + */ +@property(nonatomic, copy, nullable) NSURL *photoURL; + +/** @fn commitChangesWithCompletion: + @brief Commits any pending changes. + @remarks This method should only be called once. Once called, property values should not be + changed. + + @param completion Optionally; the block invoked when the user profile change has been applied. + Invoked asynchronously on the main thread in the future. + */ +- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUser.mm b/auth/src/ios/fake/FIRUser.mm new file mode 100644 index 0000000000..ab8967c879 --- /dev/null +++ b/auth/src/ios/fake/FIRUser.mm @@ -0,0 +1,161 @@ +/* + * Copyright 2017 Google + * + * 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 "testing/config_ios.h" +#include "testing/ticker_ios.h" +#include "testing/util_ios.h" + +#import "auth/src/ios/fake/FIRUser.h" + +#import "auth/src/ios/fake/FIRUserMetadata.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRUser { + // Manages callbacks for testing. + firebase::testing::cppsdk::CallbackTickerManager _callbackManager; +} + +// Properties from protocol need to be synthesized explicitly. +@synthesize providerID = _providerID; +@synthesize uid = _uid; +@synthesize displayName = _displayName; +@synthesize photoURL = _photoURL; +@synthesize email = _email; +@synthesize phoneNumber = _phoneNumber; + +- (instancetype)init { + self = [super init]; + if (self) { + _anonymous = YES; + _providerID = @"fake provider id"; + _uid = @"fake uid"; + _displayName = @"fake display name"; + _email = @"fake email"; + _phoneNumber = @"fake phone number"; + _metadata = [[FIRUserMetadata alloc] init]; + } + return self; +} + +- (void)updateEmail:(NSString *)email + completion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.updateEmail:completion:", + ^(NSError *_Nullable error) { + _email = email; + completion(error); + }); +} + +- (void)updatePassword:(NSString *)password + completion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.updatePassword:completion:", completion); +} + +- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneNumberCredential + completion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.updatePhoneNumberCredential:completion:", completion); +} + +- (FIRUserProfileChangeRequest *)profileChangeRequest { + return [[FIRUserProfileChangeRequest alloc] initWithCallbackManager:&_callbackManager]; +} + +- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.reloadWithCompletion:", completion); +} + +- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRUser.reauthenticateWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init]); +} + +- (void) + reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential + completion:(nullable FIRAuthDataResultCallback) completion { + _callbackManager.Add(@"FIRUser.reauthenticateAndRetrieveDataWithCredential:completion:", + completion, [[FIRAuthDataResult alloc] init]); +} + +- (void)getIDTokenResultWithCompletion:(nullable FIRAuthTokenResultCallback)completion {} + +- (void)getIDTokenResultForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenResultCallback)completion {} + +- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenCallback)completion { + _callbackManager.Add(@"FIRUser.getIDTokenForcingRefresh:completion:", completion, + @"a fake token"); +} + +- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {} + +- (void)linkWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRUser.linkWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init]); +} + +- (void)sendEmailVerificationWithCompletion: + (nullable FIRSendEmailVerificationCallback)completion { + _callbackManager.Add(@"FIRUser.sendEmailVerificationWithCompletion:", completion); +} + +- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *) credential + completion:(nullable FIRAuthDataResultCallback) completion { + _callbackManager.Add(@"FIRUser.linkAndRetrieveDataWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init]); +} + +- (void)unlinkFromProvider:(NSString *)provider + completion:(nullable FIRAuthResultCallback)completion { + _callbackManager.Add(@"FIRUser.unlinkFromProvider:completion:", completion, + [[FIRUser alloc] init]); +} + +- (void)sendEmailVerificationWithActionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendEmailVerificationCallback) + completion { +} + +- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.deleteWithCompletion:", completion); +} + +@end + +@implementation FIRUserProfileChangeRequest { + // Manages callbacks for testing. Does not own it. + firebase::testing::cppsdk::CallbackTickerManager *_callbackManager; +} + +- (instancetype) + initWithCallbackManager:(firebase::testing::cppsdk::CallbackTickerManager *)callbackManager { + self = [super init]; + if (self) { + _callbackManager = callbackManager; + } + return self; +} + +- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager->Add(@"FIRUserProfileChangeRequest.commitChangesWithCompletion:", completion); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUserInfo.h b/auth/src/ios/fake/FIRUserInfo.h new file mode 100644 index 0000000000..04eca495de --- /dev/null +++ b/auth/src/ios/fake/FIRUserInfo.h @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief Represents user data returned from an identity provider. + */ +NS_SWIFT_NAME(UserInfo) +@protocol FIRUserInfo + +/** @property providerID + @brief The provider identifier. + */ +@property(nonatomic, copy, readonly) NSString *providerID; + +/** @property uid + @brief The provider's user ID for the user. + */ +@property(nonatomic, copy, readonly) NSString *uid; + +/** @property displayName + @brief The name of the user. + */ +@property(nonatomic, copy, readonly, nullable) NSString *displayName; + +/** @property photoURL + @brief The URL of the user's profile photo. + */ +@property(nonatomic, copy, readonly, nullable) NSURL *photoURL; + +/** @property email + @brief The user's email address. + */ +@property(nonatomic, copy, readonly, nullable) NSString *email; + +/** @property phoneNumber + @brief A phone number associated with the user. + @remarks This property is only available for users authenticated via phone number auth. + */ +@property(nonatomic, readonly, nullable) NSString *phoneNumber; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUserMetadata.h b/auth/src/ios/fake/FIRUserMetadata.h new file mode 100644 index 0000000000..96b6eb1058 --- /dev/null +++ b/auth/src/ios/fake/FIRUserMetadata.h @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRUserMetadata + @brief A data class representing the metadata corresponding to a Firebase user. + */ +NS_SWIFT_NAME(UserMetadata) +@interface FIRUserMetadata : NSObject + +/** @property lastSignInDate + @brief Stores the last sign in date for the corresponding Firebase user. + */ +@property(copy, nonatomic, readonly, nullable) NSDate *lastSignInDate; + +/** @property creationDate + @brief Stores the creation date for the corresponding Firebase user. + */ +@property(copy, nonatomic, readonly, nullable) NSDate *creationDate; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be initialized manually, an instance of this class can be obtained + from a Firebase user object. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUserMetadata.mm b/auth/src/ios/fake/FIRUserMetadata.mm new file mode 100644 index 0000000000..fd5aa88934 --- /dev/null +++ b/auth/src/ios/fake/FIRUserMetadata.mm @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRUserMetadata.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRUserMetadata + +- (instancetype)init { + self = [super init]; + if (self) { + _lastSignInDate = [NSDate dateWithTimeIntervalSince1970:1]; + _creationDate = [NSDate dateWithTimeIntervalSince1970:1]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FirebaseAuth.h b/auth/src/ios/fake/FirebaseAuth.h new file mode 100644 index 0000000000..462d2ecf86 --- /dev/null +++ b/auth/src/ios/fake/FirebaseAuth.h @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRActionCodeSettings.h" +#import "FIRAdditionalUserInfo.h" +#import "FIRAuth.h" +#import "FIRAuthCredential.h" +#import "FIRAuthDataResult.h" +#import "FIRAuthErrors.h" +#import "FIRAuthTokenResult.h" +#import "FirebaseAuthVersion.h" +#import "FIREmailAuthProvider.h" +#import "FIRFacebookAuthProvider.h" +#import "FIRFederatedAuthProvider.h" +#import "FIRGameCenterAuthProvider.h" +#import "FIRGitHubAuthProvider.h" +#import "FIRGoogleAuthProvider.h" +#import "FIROAuthCredential.h" +#import "FIROAuthProvider.h" +#import "FIRTwitterAuthProvider.h" +#import "FIRUser.h" +#import "FIRUserInfo.h" +#import "FIRUserMetadata.h" + +#if TARGET_OS_IOS +#import "FIRAuthUIDelegate.h" +#import "FIRPhoneAuthCredential.h" +#import "FIRPhoneAuthProvider.h" +#import "FIRAuthAPNSTokenType.h" +#import "FIRAuthSettings.h" +#endif diff --git a/auth/src/ios/fake/FirebaseAuthVersion.h b/auth/src/ios/fake/FirebaseAuthVersion.h new file mode 100644 index 0000000000..7b4b94e908 --- /dev/null +++ b/auth/src/ios/fake/FirebaseAuthVersion.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +/** + Version number for FirebaseAuth. + */ +extern const double FirebaseAuthVersionNum; + +/** + Version string for FirebaseAuth. + */ +extern const char *const FirebaseAuthVersionStr; diff --git a/auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java b/auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java new file mode 100644 index 0000000000..263e3035e2 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseApiNotAvailableException */ +public class FirebaseApiNotAvailableException extends FirebaseException { + + public FirebaseApiNotAvailableException(String message) { + super(message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java b/auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java new file mode 100644 index 0000000000..80571d4871 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseNetworkException */ +public class FirebaseNetworkException extends FirebaseException { + + public FirebaseNetworkException(String message) { + super(message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java b/auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java new file mode 100644 index 0000000000..339c566a92 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseTooManyRequestsException */ +public class FirebaseTooManyRequestsException extends FirebaseException { + + public FirebaseTooManyRequestsException(String message) { + super(message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java b/auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java new file mode 100644 index 0000000000..c4eaeb29cf --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import java.util.Map; + +/** Fake AdditionalUserInfo */ +public final class AdditionalUserInfo { + + public String getProviderId() { + return "fake provider id"; + } + + public Map getProfile() { + return null; + } + + public String getUsername() { + return "fake user name"; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/AuthCredential.java b/auth/src_java/fake/com/google/firebase/auth/AuthCredential.java new file mode 100644 index 0000000000..8022311410 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/AuthCredential.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake AuthCredential */ +public final class AuthCredential { + private String provider; + + /** C++ code does not rely on any constructor. This is solely for fake to specify provider and + * does not map to a constructor in the real AuthCredential. */ + AuthCredential(String provider) { + this.provider = provider; + } + + public String getSignInMethod() { + return this.provider; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/AuthResult.java b/auth/src_java/fake/com/google/firebase/auth/AuthResult.java new file mode 100644 index 0000000000..fa3bf9fa92 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/AuthResult.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake AuthResult */ +public final class AuthResult { + + FirebaseUser getUser() { + return new FirebaseUser(); + } + + AdditionalUserInfo getAdditionalUserInfo() { + return new AdditionalUserInfo(); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java new file mode 100644 index 0000000000..c785cfb7aa --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake EmailAuthProvider */ +public final class EmailAuthProvider { + + public static AuthCredential getCredential(String email, String password) { + return new AuthCredential("password"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java new file mode 100644 index 0000000000..ee9997561b --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FacebookAuthProvider */ +public final class FacebookAuthProvider { + + public static AuthCredential getCredential(String accessToken) { + return new AuthCredential("facebook.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java new file mode 100644 index 0000000000..b53120cfdc --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ +package com.google.firebase.auth; + +/** + * Abstract representation of an arbitrary federated authentication provider. Generate instances + * using {@link OAuthProvider.Builder}. + */ +public abstract class FederatedAuthProvider {} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java new file mode 100644 index 0000000000..63b3b1ffa6 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java @@ -0,0 +1,242 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.app.Activity; +import com.google.android.gms.tasks.Task; +import com.google.firebase.FirebaseApiNotAvailableException; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseNetworkException; +import com.google.firebase.FirebaseTooManyRequestsException; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeListener; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import java.util.ArrayList; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Fake FirebaseAuth */ +public final class FirebaseAuth { + // This makes the signed-in status consistent and thus makes setting up test data config easier. + private boolean signedIn = false; + + // Random number generator for listener callback delays. + private final Random randomDelay = new Random(); + + private final ArrayList authStateListeners = new ArrayList<>(); + private final ArrayList idTokenListeners = new ArrayList<>(); + + public static FirebaseAuth getInstance(FirebaseApp firebaseApp) { + return new FirebaseAuth(); + } + + public FirebaseUser getCurrentUser() { + if (signedIn) { + return new FirebaseUser(); + } else { + return null; + } + } + + public static Task applyAuthExceptionFromConfig(Task task, String exceptionMsg) { + Pattern r = Pattern.compile("^\\[(.*)[?!:]:?(.*)\\] (.*)"); + Matcher matcher = r.matcher(exceptionMsg); + if (matcher.find()) { + String exceptionName = matcher.group(1); + String errorCode = matcher.group(2); + String errorMessage = matcher.group(3); + if (exceptionName.equals("FirebaseAuthInvalidCredentialsException")) { + task.setException(new FirebaseAuthInvalidCredentialsException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthActionCodeException")) { + task.setException(new FirebaseAuthActionCodeException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthEmailException")) { + task.setException(new FirebaseAuthEmailException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthException")) { + task.setException(new FirebaseAuthException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthInvalidUserException")) { + task.setException(new FirebaseAuthInvalidUserException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthRecentLoginRequiredException")) { + task.setException(new FirebaseAuthRecentLoginRequiredException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthUserCollisionException")) { + task.setException(new FirebaseAuthUserCollisionException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthWeakPasswordException")) { + task.setException(new FirebaseAuthWeakPasswordException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseApiNotAvailableException")) { + task.setException(new FirebaseApiNotAvailableException(errorMessage)); + } else if (exceptionName.equals("FirebaseException")) { + task.setException(new FirebaseException(errorMessage)); + } else if (exceptionName.equals("FirebaseNetworkException")) { + task.setException(new FirebaseNetworkException(errorMessage)); + } else if (exceptionName.equals("FirebaseTooManyRequestsException")) { + task.setException(new FirebaseTooManyRequestsException(errorMessage)); + } + } + return task; + } + + /** + * Delay the calling thread between 0..100ms. + */ + private void randomDelayThread() { + try { + Thread.sleep(randomDelay.nextInt(100)); + } catch (InterruptedException e) { + // ignore + } + } + + public void addAuthStateListener(final AuthStateListener listener) { + authStateListeners.add(listener); + new Thread( + new Runnable() { + @Override + public void run() { + randomDelayThread(); + listener.onAuthStateChanged(FirebaseAuth.this); + } + }) + .start(); + } + + public void removeAuthStateListener(AuthStateListener listener) { + authStateListeners.remove(listener); + } + + public void addIdTokenListener(final IdTokenListener listener) { + idTokenListeners.add(listener); + new Thread( + new Runnable() { + @Override + public void run() { + randomDelayThread(); + listener.onIdTokenChanged(FirebaseAuth.this); + } + }) + .start(); + } + + public void removeIdTokenListener(IdTokenListener listener) { + idTokenListeners.remove(listener); + } + + public void signOut() { + signedIn = false; + } + + public Task fetchSignInMethodsForEmail(String email) { + return null; + } + + /** A generic helper function to mimic all types of sign-in actions. */ + private Task signInHelper(String configKey) { + Task result = Task.forResult(configKey, new AuthResult()); + + ConfigRow row = ConfigAndroid.get(configKey); + if (!row.futuregeneric().throwexception()) { + result.addListener(new FakeListener() { + @Override + public void onSuccess(AuthResult res) { + signedIn = true; + for (AuthStateListener listener : authStateListeners) { + listener.onAuthStateChanged(FirebaseAuth.this); + } + } + }); + } else { + result = applyAuthExceptionFromConfig(result, row.futuregeneric().exceptionmsg()); + } + + TickerAndroid.register(result); + return result; + } + + public Task signInWithCustomToken(String token) { + return signInHelper("FirebaseAuth.signInWithCustomToken"); + } + + public Task signInWithCredential(AuthCredential credential) { + return signInHelper("FirebaseAuth.signInWithCredential"); + } + + public Task signInAnonymously() { + return signInHelper("FirebaseAuth.signInAnonymously"); + } + + public Task signInWithEmailAndPassword(String email, String password) { + return signInHelper("FirebaseAuth.signInWithEmailAndPassword"); + } + + /** + * Signs in the user using the mobile browser (either a Custom Chrome Tab or the device's default + * browser) for the given {@code provider}. + * + *

    Note: this call has a UI associated with it, unlike the majority of calls in FirebaseAuth. + * + *

    Exceptions
    + * + *
      + *
    • {@link FirebaseAuthInvalidCredentialsException} thrown if the credential generated from + * the flow is malformed or expired. + *
    • {@link FirebaseAuthInvalidUserException} thrown if the user has been disabled by an + * administrator. + *
    • {@link FirebaseAuthUserCollisionException} thrown if the email that keys the user that is + * signing in is already in use. + *
    • {@link FirebaseAuthWebException} thrown if there is an operation already in progress, the + * pending operation was canceled, there is a problem with 3rd party cookies in the browser, + * or some other error in the web context has occurred. + *
    • {@link FirebaseAuthException} thrown if signing in via this method has been disabled in + * the Firebase Console, or if the {@code provider} passed is configured improperly. + *
    + * + * @param activity the current {@link Activity} from which you intend to launch this flow. + * @param federatedAuthProvider an {@link FederatedAuthProvider} configured with information about + * how you intend the user to sign in. + * @return a {@link Task} with a reference to an {@link AuthResult} with user information upon + * success + */ + public Task startActivityForSignInWithProvider( + Activity activity, FederatedAuthProvider federatedAuthProvider) { + return signInHelper("FirebaseAuth.startActivityForSignInWithProvider"); + } + + public Task createUserWithEmailAndPassword(String email, String password) { + return signInHelper("FirebaseAuth.createUserWithEmailAndPassword"); + } + + public Task sendPasswordResetEmail(String email) { + Task result = Task.forResult("FirebaseAuth.sendPasswordResetEmail", null); + ConfigRow row = ConfigAndroid.get("FirebaseAuth.sendPasswordResetEmail"); + if (row.futuregeneric().throwexception()) { + result = applyAuthExceptionFromConfig(result, row.futuregeneric().exceptionmsg()); + } + TickerAndroid.register(result); + return result; + } + + /** AuthStateListener */ + public interface AuthStateListener { + void onAuthStateChanged(FirebaseAuth auth); + } + + /** IdTokenListener */ + public interface IdTokenListener { + void onIdTokenChanged(FirebaseAuth auth); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java new file mode 100644 index 0000000000..30cc2e5398 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthActionCodeException */ +public class FirebaseAuthActionCodeException extends FirebaseAuthException { + + public FirebaseAuthActionCodeException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java new file mode 100644 index 0000000000..e1d46ad849 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthEmailException */ +public class FirebaseAuthEmailException extends FirebaseAuthException { + + public FirebaseAuthEmailException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java new file mode 100644 index 0000000000..f519095c5a --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import com.google.firebase.FirebaseException; + +/** Fake FirebaseAuthException */ +public class FirebaseAuthException extends FirebaseException { + + public FirebaseAuthException(String code, String message) { + super(message); + code_ = code; + } + + public String getErrorCode() { + return code_; + } + + private String code_; +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java new file mode 100644 index 0000000000..8e37cda351 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthInvalidCredentialsException */ +public class FirebaseAuthInvalidCredentialsException extends FirebaseAuthException { + + public FirebaseAuthInvalidCredentialsException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java new file mode 100644 index 0000000000..30566fa193 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthInvalidUserException */ +public final class FirebaseAuthInvalidUserException extends FirebaseAuthException { + + public FirebaseAuthInvalidUserException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java new file mode 100644 index 0000000000..e1a7cecd13 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthRecentLoginRequiredException */ +public class FirebaseAuthRecentLoginRequiredException extends FirebaseAuthException { + + public FirebaseAuthRecentLoginRequiredException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java new file mode 100644 index 0000000000..63e94ce39d --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthUserCollisionException */ +public class FirebaseAuthUserCollisionException extends FirebaseAuthException { + + public FirebaseAuthUserCollisionException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java new file mode 100644 index 0000000000..acfb84ebc5 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthWeakPasswordException */ +public class FirebaseAuthWeakPasswordException extends FirebaseAuthInvalidCredentialsException { + + public FirebaseAuthWeakPasswordException(String code, String message) { + super(code, message); + } + + public String getReason() { + return "fake bad password reason."; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java new file mode 100644 index 0000000000..8260a51a76 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthWebException */ +public class FirebaseAuthWebException extends FirebaseAuthException { + + public FirebaseAuthWebException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java new file mode 100644 index 0000000000..753371f789 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java @@ -0,0 +1,201 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.app.Activity; +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeListener; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import java.util.List; + +/** Fake FirebaseUser (not completed yet). */ +public final class FirebaseUser extends UserInfo { + public boolean isAnonymous() { + return true; + } + + public Task getIdToken(boolean forceRefresh) { + Task result = Task.forResult("FirebaseUser.getIdToken", new GetTokenResult()); + TickerAndroid.register(result); + return result; + } + + public List getProviderData() { + return null; + } + + public Task updateEmail(String email) { + final String configKey = "FirebaseUser.updateEmail"; + Task result = Task.forResult(configKey, null); + + ConfigRow row = ConfigAndroid.get(configKey); + if (!row.futuregeneric().throwexception()) { + result.addListener( + new FakeListener() { + @Override + public void onSuccess(Void res) { + FirebaseUser.this.email = email; + } + }); + } + + TickerAndroid.register(result); + return result; + } + + public Task updatePassword(String email) { + Task result = Task.forResult("FirebaseUser.updatePassword", null); + TickerAndroid.register(result); + return result; + } + + public Task updateProfile(UserProfileChangeRequest request) { + Task result = Task.forResult("FirebaseUser.updateProfile", null); + TickerAndroid.register(result); + return result; + } + + public Task linkWithCredential(AuthCredential credential) { + Task result = Task.forResult("FirebaseUser.linkWithCredential", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + /** + * Links the user using the mobile browser (either a Custom Chrome Tab or the device's default + * browser) to the given {@code provider}. If the calling activity dies during this operation, use + * {@link FirebaseAuth#getPendingAuthResult()} to get the outcome of this operation. + * + *

    Note: this call has a UI associated with it, unlike the majority of calls in FirebaseAuth. + * + *

    Exceptions
    + * + *
      + *
    • {@link FirebaseAuthInvalidCredentialsException} thrown if the credential generated from + * the flow is malformed or expired. + *
    • {@link FirebaseAuthInvalidUserException} thrown if the user has been disabled by an + * administrator. + *
    • {@link FirebaseAuthUserCollisionException} thrown if the email that keys the user that is + * signing in is already in use. + *
    • {@link FirebaseAuthWebException} thrown if there is an operation already in progress, the + * pending operation was canceled, there is a problem with 3rd party cookies in the browser, + * or some other error in the web context has occurred. + *
    • {@link FirebaseAuthException} thrown if signing in via this method has been disabled in + * the Firebase Console, or if the {@code provider} passed is configured improperly. + *
    + * + * @param activity the current {@link Activity} that you intent to launch this flow from + * @param federatedAuthProvider an {@link FederatedAuthProvider} configured with information about + * the provider that you intend to link to the user. + * @return a {@link Task} with a reference to an {@link AuthResult} with user information upon + * success + */ + public Task startActivityForLinkWithProvider( + Activity activity, FederatedAuthProvider federatedAuthProvider) { + Task result = + Task.forResult("FirebaseUser.startActivityForLinkWithProvider", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + public Task unlink(String provider) { + Task result = Task.forResult("FirebaseUser.unlink", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + public Task updatePhoneNumber(PhoneAuthCredential credential) { + return null; + } + + public Task reload() { + Task result = Task.forResult("FirebaseUser.reload", null); + TickerAndroid.register(result); + return result; + } + + public Task reauthenticate(AuthCredential credential) { + Task result = Task.forResult("FirebaseUser.reauthenticate", null); + TickerAndroid.register(result); + return result; + } + + public Task reauthenticateAndRetrieveData(AuthCredential credential) { + Task result = + Task.forResult("FirebaseUser.reauthenticateAndRetrieveData", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + /** + * Reauthenticates the user using the mobile browser (either a Custom Chrome Tab or the device's + * default browser) using the given {@code provider}. If the calling activity dies during this + * operation, use {@link FirebaseAuth#getPendingAuthResult()} to get the outcome of this + * operation. + * + *

    Note: this call has a UI associated with it, unlike the majority of calls in FirebaseAuth. + * + *

    Exceptions
    + * + *
      + *
    • {@link FirebaseAuthInvalidCredentialsException} thrown if the credential generated from + * the flow is malformed or expired. + *
    • {@link FirebaseAuthInvalidUserException} thrown if the user has been disabled by an + * administrator. + *
    • {@link FirebaseAuthUserCollisionException} thrown if the email that keys the user that is + * signing in is already in use. + *
    • {@link FirebaseAuthWebException} thrown if there is an operation already in progress, the + * pending operation was canceled, there is a problem with 3rd party cookies in the browser, + * or some other error in the web context has occurred. + *
    • {@link FirebaseAuthException} thrown if signing in via this method has been disabled in + * the Firebase Console, or if the {@code provider} passed is configured improperly. + *
    + * + * @param activity the current {@link Activity} that you intent to launch this flow from + * @param federatedAuthProvider an {@link FederatedAuthProvider} configured with information about + * how you intend the user to reauthenticate. + * @return a {@link Task} with a reference to an {@link AuthResult} with user information upon + * success + */ + public Task startActivityForReauthenticateWithProvider( + Activity activity, FederatedAuthProvider federatedAuthProvider) { + Task result = + Task.forResult("FirebaseUser.startActivityForReauthenticateWithProvider", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + + public Task delete() { + Task result = Task.forResult("FirebaseUser.delete", null); + TickerAndroid.register(result); + return result; + } + + public Task sendEmailVerification() { + Task result = Task.forResult("FirebaseUser.sendEmailVerification", null); + TickerAndroid.register(result); + return result; + } + + /** Returns the {@link FirebaseUserMetadata} associated with this user. */ + public FirebaseUserMetadata getMetadata() { + return new FirebaseUserMetadata(); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java new file mode 100644 index 0000000000..6f214cad5b --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Holds the user metadata for the current {@link FirebaseUser} */ +public class FirebaseUserMetadata { + + /** Fake timestamp returned that's non-zero. */ + public long getLastSignInTimestamp() { + return 1; + } + + /** Fake timestamp returned that's non-zero. */ + public long getCreationTimestamp() { + return 1; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java b/auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java new file mode 100644 index 0000000000..b925fc7627 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake GetTokenResult */ +public final class GetTokenResult { + + public String getToken() { + return "a fake token"; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java new file mode 100644 index 0000000000..8f08b9df4c --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake GithubAuthProvider */ +public final class GithubAuthProvider { + + public static AuthCredential getCredential(String accessToken) { + return new AuthCredential("github.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java new file mode 100644 index 0000000000..ad9b327934 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake GoogleAuthProvider */ +public final class GoogleAuthProvider { + + public static AuthCredential getCredential(String idToken, String accessToken) { + return new AuthCredential("google.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java new file mode 100644 index 0000000000..32867a4aca --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import java.util.List; +import java.util.Map; + +/** Fake FakeOAuthProvider */ +public final class OAuthProvider extends FederatedAuthProvider { + + public static AuthCredential getCredential( + String providerId, String idToken, String accessToken) { + return new AuthCredential(providerId); + } + + /** + * Returns a {@link OAuthProvider.Builder} used to construct a {@link OAuthProvider} instantiated + * with the given {@code providerId}. + */ + public static OAuthProvider.Builder newBuilder(String providerId, FirebaseAuth firebaseAuth) { + return new OAuthProvider.Builder(); + } + + /** Class used to create instances of {@link OAuthProvider}. */ + public static class Builder { + + /* Fake constructor */ + private Builder() {} + + /** + * Sets the OAuth 2 scopes to be presented to the user during their sign-in flow with the + * identity provider. + */ + public OAuthProvider.Builder setScopes(List scopes) { + return this; + } + + /** + * Configures custom parameters to be passed to the identity provider during the OAuth sign-in + * flow. Calling this method multiple times will add to the set of custom parameters being + * passed, rather than overwriting them (as long as key values don't collide). + * + * @param paramKey the name of the custom parameter + * @param paramValue the value of the custom parameter + */ + public OAuthProvider.Builder addCustomParameter(String paramKey, String paramValue) { + return this; + } + + /** + * Similar to {@link #addCustomParameter(String, String)}, this takes a Map and adds each entry + * to the set of custom parameters to be passed. Calling this method multiple times will add to + * the set of custom parameters being passed, rather than overwriting them (as long as key + * values don't collide). + * + * @param customParameters a dictionary of custom parameter names and values to be passed to the + * identity provider as part of the sign-in flow. + */ + public OAuthProvider.Builder addCustomParameters(Map customParameters) { + return this; + } + + /** Returns an {@link OAuthProvider} created from this {@link Builder}. */ + public OAuthProvider build() { + return new OAuthProvider(); + } + } + + /** + * Creates an {@link OAuthProvider.CredentialBuilder} for the specified provider ID. + * + * @throws IllegalArgumentException if {@code providerId} is null or empty + */ + public static CredentialBuilder newCredentialBuilder(String providerId) { + return new CredentialBuilder(providerId); + } + + /** Builder class to initialize {@link AuthCredential}'s. */ + public static class CredentialBuilder { + + private final String providerId; + + /** + * Internal constructor. + */ + private CredentialBuilder(String providerId) { + this.providerId = providerId; + } + + /** + * Adds an ID token to the credential being built. + * + *

    If this is an OIDC ID token with a nonce field, please use {@link + * #setIdTokenWithRawNonce(String, String)} instead. + */ + public OAuthProvider.CredentialBuilder setIdToken(String idToken) { + return this; + } + + /** + * Adds an ID token and raw nonce to the credential being built. + * + *

    The raw nonce is required when an OIDC ID token with a nonce field is provided. The + * SHA-256 hash of the raw nonce must match the nonce field in the OIDC ID token. + */ + public OAuthProvider.CredentialBuilder setIdTokenWithRawNonce(String idToken, String rawNonce) { + return this; + } + + /** Adds an access token to the credential being built. */ + public OAuthProvider.CredentialBuilder setAccessToken(String accessToken) { + return this; + } + + /** + * Returns the {@link AuthCredential} that this {@link CredentialBuilder} has constructed. + * + * @throws IllegalArgumentException if an ID token and access token were not provided. + */ + public AuthCredential build() { + return new AuthCredential(providerId); + } + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java new file mode 100644 index 0000000000..a55d82a58e --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake PhoneAuthCredential */ +public class PhoneAuthCredential { + public String getSmsCode() { + return "fake sms code"; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java new file mode 100644 index 0000000000..84b10a73a9 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.app.Activity; +import com.google.firebase.FirebaseException; +import java.util.concurrent.TimeUnit; + +/** Fake PhoneAuthProvider */ +public class PhoneAuthProvider { + + /** Fake OnVerificationStateChangedCallbacks */ + public abstract static class OnVerificationStateChangedCallbacks { + public abstract void onVerificationCompleted(PhoneAuthCredential credential); + + public abstract void onVerificationFailed(FirebaseException exception); + + public void onCodeSent(String verificationId, ForceResendingToken forceResendingToken) {} + + public void onCodeAutoRetrievalTimeOut(String verificationId) {} + } + + /** Fake ForceResendingToken */ + public static class ForceResendingToken {} + + public static PhoneAuthProvider getInstance(FirebaseAuth firebaseAuth) { + return null; + } + + public static PhoneAuthCredential getCredential( + String verificationId, String smsCode) { + return null; + } + + public void verifyPhoneNumber( + String phoneNumber, + long timeout, + TimeUnit unit, + Activity activity, + OnVerificationStateChangedCallbacks callbacks, + ForceResendingToken forceResendingToken) {} +} diff --git a/auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java new file mode 100644 index 0000000000..b5da4c3cf2 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake PlayGamesAuthProvider */ +class PlayGamesAuthProvider { + + public static AuthCredential getCredential(String authCode) { + return new AuthCredential("playgames.google.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java b/auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java new file mode 100644 index 0000000000..d0ba463a8f --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import java.util.List; + +/** Fake SignInMethodQueryResult */ +public final class SignInMethodQueryResult { + + List getSignInMethods() { + return null; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java new file mode 100644 index 0000000000..c3358e7c20 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake TwitterAuthProvider */ +public final class TwitterAuthProvider { + + public static AuthCredential getCredential(String token, String secret) { + return new AuthCredential("twitter.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/UserInfo.java b/auth/src_java/fake/com/google/firebase/auth/UserInfo.java new file mode 100644 index 0000000000..f2570abb2d --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/UserInfo.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.net.Uri; + +/** Fake UserInfo */ +public class UserInfo { + protected String email = "fake email"; + + String getUid() { + return "fake uid"; + } + + String getProviderId() { + return "fake provider id"; + } + + String getDisplayName() { + return "fake display name"; + } + + String getPhoneNumber() { + return "fake phone number"; + } + + Uri getPhotoUrl() { + return null; + } + + String getEmail() { + return email; + } + + boolean isEmailVerified() { + // This is false to match the desktop stub. + return false; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java b/auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java new file mode 100644 index 0000000000..dfab7c05b0 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.net.Uri; + +/** Fake UserProfileChangeRequest$Builder */ +public final class UserProfileChangeRequest { + + /** Builder */ + public static class Builder { + public Builder setDisplayName(String displayName) { + return this; + } + + public Builder setPhotoUri(Uri photoUri) { + return this; + } + + public UserProfileChangeRequest build() { + return new UserProfileChangeRequest(); + } + } +} diff --git a/auth/tests/CMakeLists.txt b/auth/tests/CMakeLists.txt new file mode 100644 index 0000000000..15f5fd6820 --- /dev/null +++ b/auth/tests/CMakeLists.txt @@ -0,0 +1,282 @@ +# Copyright 2019 Google LLC +# +# 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. + +set(desktop_fakes_SRCS + desktop/fakes.h + desktop/fakes.cc) +set(desktop_test_util_SRCS + desktop/test_utils.h + desktop/test_utils.cc + ) +set(ios_frameworks + FirebaseAuth + ) + +add_library(firebase_auth_desktop_test_util STATIC + ${desktop_fakes_SRCS} + ${desktop_test_util_SRCS}) + +target_include_directories(firebase_auth_desktop_test_util + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_GEN_FILE_DIR} +) + +target_link_libraries(firebase_auth_desktop_test_util + PRIVATE + firebase_auth + firebase_testing + gtest + gmock +) + +target_compile_definitions(firebase_auth_desktop_test_util + PRIVATE + -DINTERNAL_EXPERIMENTAL=1 +) + + +if (NOT ANDROID AND NOT IOS) + set(desktop_rpc_test_util_SRCS + desktop/rpcs/test_util.h + desktop/rpcs/test_util.cc) + + add_library(firebase_auth_desktop_rpc_test_util STATIC + ${desktop_rpc_test_util_SRCS}) + + target_include_directories(firebase_auth_desktop_rpc_test_util + PRIVATE + ${FLATBUFFERS_SOURCE_DIR}/include + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_GEN_FILE_DIR} + ) + + target_link_libraries(firebase_auth_desktop_rpc_test_util + PRIVATE + firebase_auth + ) +endif() + + +firebase_cpp_cc_test( + firebase_auth_test + SOURCES + auth_test.cc + DEPENDS + firebase_app_for_testing + firebase_rest_mocks + firebase_auth + firebase_testing + DEFINES + -DFIREBASE_WAIT_ASYNC_IN_TEST +) + +firebase_cpp_cc_test_on_ios( + firebase_auth_test + HOST + firebase_app_for_testing_ios + SOURCES + credential_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing + CUSTOM_FRAMEWORKS + ${ios_frameworks} +) + +firebase_cpp_cc_test( + firebase_auth_credential_test + SOURCES + credential_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test_on_ios( + firebase_auth_credential_test + HOST + firebase_app_for_testing_ios + SOURCES + credential_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing + CUSTOM_FRAMEWORKS + ${ios_frameworks} +) + +firebase_cpp_cc_test( + firebase_auth_user_test + SOURCES + user_test.cc + DEPENDS + firebase_app_for_testing + firebase_rest_mocks + firebase_auth + firebase_testing + DEFINES + -DFIREBASE_WAIT_ASYNC_IN_TEST +) + +firebase_cpp_cc_test_on_ios( + firebase_auth_user_test + HOST + firebase_app_for_testing_ios + SOURCES + user_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing + CUSTOM_FRAMEWORKS + ${ios_frameworks} +) + +firebase_cpp_cc_test( + firebase_auth_desktop_test + SOURCES + desktop/auth_desktop_test.cc + DEPENDS + firebase_auth + firebase_auth_desktop_test_util + firebase_rest_lib + firebase_rest_mocks + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_user_desktop_test + SOURCES + desktop/user_desktop_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_auth_desktop_test_util + firebase_rest_lib + firebase_rest_mocks + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_create_auth_uri_test + SOURCES + desktop/rpcs/create_auth_uri_test.cc + DEPENDS + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_delete_account_test_test + SOURCES + desktop/rpcs/delete_account_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_get_account_info_test + SOURCES + desktop/rpcs/get_account_info_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_get_oob_confirmation_code_test + SOURCES + desktop/rpcs/get_oob_confirmation_code_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_reset_password_test + SOURCES + desktop/rpcs/reset_password_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_secure_token_test + SOURCES + desktop/rpcs/secure_token_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_set_account_info_test + SOURCES + desktop/rpcs/set_account_info_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_sign_up_new_user_test + SOURCES + desktop/rpcs/sign_up_new_user_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_verify_assertion_test + SOURCES + desktop/rpcs/verify_assertion_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_verify_custom_token_test + SOURCES + desktop/rpcs/verify_custom_token_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_verify_password_test + SOURCES + desktop/rpcs/verify_password_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) diff --git a/auth/tests/auth_test.cc b/auth/tests/auth_test.cc new file mode 100644 index 0000000000..1f61752446 --- /dev/null +++ b/auth/tests/auth_test.cc @@ -0,0 +1,558 @@ +/* + * Copyright 2017 Google LLC + * + * 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 + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/include/firebase/auth.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/ticker.h" + +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) +#include "app/rest/transport_builder.h" +#include "app/rest/transport_mock.h" +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +namespace firebase { +namespace auth { + +namespace { + +// Wait for the Future completed when necessary. We do not do so for Android nor +// iOS since their test is based on Ticker-based fake. We do not do so for +// desktop stub since its Future completes immediately. +template +inline void MaybeWaitForFuture(const Future& future) { +// Desktop developer sdk has a small delay due to async calls. +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + // Once REST implementation is in, we should be able to check this. Almost + // always the return of last-result is ahead of the future completion. But + // right now, the return of last-result actually happens after future is + // completed. + // EXPECT_EQ(firebase::kFutureStatusPending, future.status()); + while (firebase::kFutureStatusPending == future.status()) {} +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) +} + +// Helper functions to verify the auth future result. +template +void Verify(const AuthError error, const Future& result, + bool check_result_not_null) { +// Desktop stub returns result immediately and thus we skip the ticker elapse. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + MaybeWaitForFuture(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(error, result.error()); + if (check_result_not_null) { + EXPECT_NE(nullptr, result.result()); + } +} + +template +void Verify(const AuthError error, const Future& result) { + Verify(error, result, true /* check_result_not_null */); +} + +template <> +void Verify(const AuthError error, const Future& result) { + Verify(error, result, false /* check_result_not_null */); +} + +} // anonymous namespace + +class AuthTest : public ::testing::Test { + protected: + void SetUp() override { +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + } + + void TearDown() override { + delete firebase_auth_; + firebase_auth_ = nullptr; + delete firebase_app_; + firebase_app_ = nullptr; + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + // Helper function for those test case that needs an Auth but not care on the + // creation of that. + void MakeAuth() { + firebase_app_ = testing::CreateApp(); + firebase_auth_ = Auth::GetAuth(firebase_app_); + } + + App* firebase_app_ = nullptr; + Auth* firebase_auth_ = nullptr; +}; + +TEST_F(AuthTest, TestAuthCreation) { + // This test verifies the creation of an Auth object. + App* firebase_app = testing::CreateApp(); + EXPECT_NE(nullptr, firebase_app); + + Auth* firebase_auth = Auth::GetAuth(firebase_app); + EXPECT_NE(nullptr, firebase_auth); + + // Calling again does not create a new Auth object. + Auth* firebase_auth_again = Auth::GetAuth(firebase_app); + EXPECT_EQ(firebase_auth, firebase_auth_again); + + delete firebase_auth; + delete firebase_app; +} + +// Creates and destroys multiple auth objects to ensure destruction doesn't +// result in data races due to callbacks from the Java layer. +TEST_F(AuthTest, TestAuthCreateDestroy) { + static int kTestIterations = 100; + // Pipeline of app and auth objects that are all active at once. + struct { + App *app; + Auth *auth; + } created_queue[10]; + memset(created_queue, 0, sizeof(created_queue)); + size_t created_queue_items = sizeof(created_queue) / sizeof(created_queue[0]); + + // Create and destroy app and auth objects keeping up to created_queue_items + // alive at a time. + for (size_t i = 0; i < kTestIterations; ++i) { + auto* queue_entry = &created_queue[i % created_queue_items]; + delete queue_entry->auth; + delete queue_entry->app; + queue_entry->app = + testing::CreateApp(testing::MockAppOptions(), + (std::string("app") + std::to_string(i)).c_str()); + queue_entry->auth = Auth::GetAuth(queue_entry->app); + EXPECT_NE(nullptr, queue_entry->auth); + } + + // Clean up the queue. + for (size_t i = 0; i < created_queue_items; ++i) { + auto* queue_entry = &created_queue[i % created_queue_items]; + delete queue_entry->auth; + queue_entry->auth = nullptr; + delete queue_entry->app; + queue_entry->app = nullptr; + } +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +TEST_F(AuthTest, TestAuthCreationWithNoGooglePlay) { + // This test is specific to Android platform. Without Google Play, we cannot + // create an Auth object. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:1}}" + " ]" + "}"); + App* firebase_app = testing::CreateApp(); + EXPECT_NE(nullptr, firebase_app); + + Auth* firebase_auth = Auth::GetAuth(firebase_app); + EXPECT_EQ(nullptr, firebase_auth); + + delete firebase_app; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +// Below are tests for testing different login methods and in different status. + +TEST_F(AuthTest, TestSignInWithCustomTokenSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCustomToken'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInWithCustomToken:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyCustomToken?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInWithCustomToken("its-a-token"); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestSignInWithCredentialSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCredential'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInWithCredential:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Credential credential = EmailAuthProvider::GetCredential("abc@g.com", "abc"); + Future result = firebase_auth_->SignInWithCredential(credential); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestSignInAnonymouslySucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInAnonymously(); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestSignInWithEmailAndPasswordSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithEmailAndPassword'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInWithEmail:password:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->SignInWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestCreateUserWithEmailAndPasswordSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.createUserWithEmailAndPassword'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.createUserWithEmail:password:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->CreateUserWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorNone, result); +} + +// Right now the desktop stub always succeeded. We could potentially test it by +// adding a desktop fake, which does not provide much value for the specific +// case of Auth since the C++ code is only a thin wraper. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + +TEST_F(AuthTest, TestSignInWithCustomTokenFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCustomToken'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "CUSTOM_TOKEN] sign-in with custom token failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInWithCustomToken:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "CUSTOM_TOKEN] sign-in with custom token failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInWithCustomToken("its-a-token"); + Verify(kAuthErrorInvalidCustomToken, result); +} + +TEST_F(AuthTest, TestSignInWithCredentialFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCredential'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "EMAIL] sign-in with credential failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInWithCredential:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "EMAIL] sign-in with credential failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Credential credential = EmailAuthProvider::GetCredential("abc@g.com", "abc"); + Future result = firebase_auth_->SignInWithCredential(credential); + Verify(kAuthErrorInvalidEmail, result); +} + +TEST_F(AuthTest, TestSignInAnonymouslyFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthException:ERROR_OPERATION_NOT_ALLOWED] " + "sign-in anonymously failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthException:ERROR_OPERATION_NOT_ALLOWED] " + "sign-in anonymously failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInAnonymously(); + Verify(kAuthErrorOperationNotAllowed, result); +} + +TEST_F(AuthTest, TestSignInWithEmailAndPasswordFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithEmailAndPassword'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_WRONG_" + "PASSWORD] sign-in with email/password failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInWithEmail:password:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_WRONG_" + "PASSWORD] sign-in with email/password failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->SignInWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorWrongPassword, result); +} + +TEST_F(AuthTest, TestCreateUserWithEmailAndPasswordFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.createUserWithEmailAndPassword'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthUserCollisionException:ERROR_EMAIL_ALREADY_" + "IN_USE] create user with email/pwd failed'," + " ticker:1}}," + " {fake:'FIRAuth.createUserWithEmail:password:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthUserCollisionException:ERROR_EMAIL_ALREADY_" + "IN_USE] create user with email/pwd failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->CreateUserWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorEmailAlreadyInUse, result); +} + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + +TEST_F(AuthTest, TestCurrentUserAndSignOut) { + // Here we let mock sign-in-anonymously succeed immediately (ticker = 0). + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{ticker:0}}," + " {fake:'FIRAuth.FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{ticker:0}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + + // No user is signed in. + EXPECT_EQ(nullptr, firebase_auth_->current_user()); + + // Now sign-in, say anonymously. + Future result = firebase_auth_->SignInAnonymously(); + MaybeWaitForFuture(result); + EXPECT_NE(nullptr, firebase_auth_->current_user()); + + // Now sign-out. + firebase_auth_->SignOut(); + EXPECT_EQ(nullptr, firebase_auth_->current_user()); +} + +TEST_F(AuthTest, TestSendPasswordResetEmailSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.sendPasswordResetEmail'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.sendPasswordResetWithEmail:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"email\": \"my@email.com\"}']" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SendPasswordResetEmail("my@email.com"); + Verify(kAuthErrorNone, result); +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS +TEST_F(AuthTest, TestSendPasswordResetEmailFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.sendPasswordResetEmail'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthEmailException:ERROR_INVALID_MESSAGE_PAYLOAD]" + " failed to send password reset email'," + " ticker:1}}," + " {fake:'FIRAuth.sendPasswordResetWithEmail:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthEmailException:ERROR_INVALID_MESSAGE_PAYLOAD]" + " failed to send password reset email'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SendPasswordResetEmail("my@email.com"); + Verify(kAuthErrorInvalidMessagePayload, result); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/credential_test.cc b/auth/tests/credential_test.cc new file mode 100644 index 0000000000..f3a0587f73 --- /dev/null +++ b/auth/tests/credential_test.cc @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/credential.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/ticker.h" + +namespace firebase { +namespace auth { + +class CredentialTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + firebase_app_ = testing::CreateApp(); + firebase_auth_ = Auth::GetAuth(firebase_app_); + EXPECT_NE(nullptr, firebase_auth_); + } + + void TearDown() override { + firebase::testing::cppsdk::ConfigReset(); + delete firebase_auth_; + firebase_auth_ = nullptr; + delete firebase_app_; + firebase_app_ = nullptr; + } + + // Helper function to verify the credential result. + void Verify(const Credential& credential, const char* provider) { + EXPECT_TRUE(credential.is_valid()); + EXPECT_EQ(provider, credential.provider()); + } + + App* firebase_app_ = nullptr; + Auth* firebase_auth_ = nullptr; +}; + +TEST_F(CredentialTest, TestEmailAuthProvider) { + // Test get credential from email and password. + Credential credential = EmailAuthProvider::GetCredential("i@email.com", "pw"); + Verify(credential, "password"); +} + +TEST_F(CredentialTest, TestFacebookAuthProvider) { + // Test get credential via Facebook. + Credential credential = FacebookAuthProvider::GetCredential("aFacebookToken"); + Verify(credential, "facebook.com"); +} + +TEST_F(CredentialTest, TestGithubAuthProvider) { + // Test get credential via GitHub. + Credential credential = GitHubAuthProvider::GetCredential("aGitHubToken"); + Verify(credential, "github.com"); +} + +TEST_F(CredentialTest, TestGoogleAuthProvider) { + // Test get credential via Google. + Credential credential = GoogleAuthProvider::GetCredential("red", "blue"); + Verify(credential, "google.com"); +} + +#if defined(__ANDROID__) || defined(FIREBASE_ANDROID_FOR_DESKTOP) +TEST_F(CredentialTest, TestPlayGamesAuthProvider) { + // Test get credential via PlayGames. + Credential credential = PlayGamesAuthProvider::GetCredential("anAuthCode"); + Verify(credential, "playgames.google.com"); +} +#endif // defined(__ANDROID__) || defined(FIREBASE_ANDROID_FOR_DESKTOP) + +TEST_F(CredentialTest, TestTwitterAuthProvider) { + // Test get credential via Twitter. + Credential credential = TwitterAuthProvider::GetCredential("token", "secret"); + Verify(credential, "twitter.com"); +} + +TEST_F(CredentialTest, TestOAuthProvider) { + // Test get credential via OAuth. + Credential credential = OAuthProvider::GetCredential("u.test", "id", "acc"); + Verify(credential, "u.test"); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/auth_desktop_test.cc b/auth/tests/desktop/auth_desktop_test.cc new file mode 100644 index 0000000000..07d746ee1a --- /dev/null +++ b/auth/tests/desktop/auth_desktop_test.cc @@ -0,0 +1,895 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/src/desktop/auth_desktop.h" + +#include +#include +#include +#include + +#include "app/memory/unique_ptr.h" +#include "app/rest/transport_builder.h" +#include "app/rest/transport_curl.h" +#include "app/rest/transport_mock.h" +#include "app/src/include/firebase/app.h" +#include "app/src/mutex.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/desktop/sign_in_flow.h" +#include "auth/src/desktop/user_desktop.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/types.h" +#include "auth/src/include/firebase/auth/user.h" +#include "auth/tests/desktop/fakes.h" +#include "auth/tests/desktop/test_utils.h" +#include "testing/config.h" +#include "testing/ticker.h" +#include "flatbuffers/stl_emulation.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace auth { + +using test::CreateErrorHttpResponse; +using test::FakeSetT; +using test::FakeSuccessfulResponse; +using test::GetFakeOAuthProviderData; +using test::GetUrlForApi; +using test::InitializeConfigWithAFake; +using test::InitializeConfigWithFakes; +using test::OAuthProviderTestHandler; +using test::VerifySignInResult; +using test::WaitForFuture; +using ::testing::IsEmpty; + +namespace { + +const char* const API_KEY = "MY-FAKE-API-KEY"; +// Constant, describing how many times we would like to sleep 1ms to wait +// for loading persistence cache. +const int kWaitForLoadMaxTryout = 500; + +void VerifyProviderData(const User& user) { + const std::vector& provider_data = user.provider_data(); + EXPECT_EQ(1, provider_data.size()); + if (provider_data.empty()) { + return; // Avoid crashing on vector out-of-bounds access below + } + EXPECT_EQ("fake_uid", provider_data[0]->uid()); + EXPECT_EQ("fake_email@example.com", provider_data[0]->email()); + EXPECT_EQ("fake_display_name", provider_data[0]->display_name()); + EXPECT_EQ("fake_photo_url", provider_data[0]->photo_url()); + EXPECT_EQ("fake_provider_id", provider_data[0]->provider_id()); + EXPECT_EQ("123123", provider_data[0]->phone_number()); +} + +void VerifyUser(const User& user) { + EXPECT_EQ("localid123", user.uid()); + EXPECT_EQ("testsignin@example.com", user.email()); + EXPECT_EQ("", user.display_name()); + EXPECT_EQ("", user.photo_url()); + EXPECT_EQ("Firebase", user.provider_id()); + EXPECT_EQ("", user.phone_number()); + EXPECT_FALSE(user.is_email_verified()); + VerifyProviderData(user); +} + +std::string GetFakeProviderInfo() { + return "\"providerUserInfo\": [" + " {" + " \"federatedId\": \"fake_uid\"," + " \"email\": \"fake_email@example.com\"," + " \"displayName\": \"fake_display_name\"," + " \"photoUrl\": \"fake_photo_url\"," + " \"providerId\": \"fake_provider_id\"," + " \"phoneNumber\": \"123123\"" + " }" + "]"; +} + +std::string CreateGetAccountInfoFake() { + return FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\",") + + GetFakeProviderInfo() + + " }" + " ]"); +} + +std::string CreateVerifyAssertionResponse() { + return FakeSuccessfulResponse("VerifyAssertionResponse", + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"idtoken123\"," + "\"providerId\": \"google.com\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); +} + +std::string CreateVerifyAssertionResponseWithUserInfo( + const std::string& provider_id, const std::string& raw_user_info) { + const auto head = std::string( + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"idtoken123\"," + "\"providerId\": \"") + + provider_id + "\","; + + std::string user_info; + if (!raw_user_info.empty()) { + user_info = "\"rawUserInfo\": \"{" + raw_user_info + "}\","; + } + + const auto tail = + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""; + + const auto body = head + user_info + tail; + return FakeSuccessfulResponse("VerifyAssertionResponse", body); +} + +void InitializeSignInWithProviderFakes( + const std::string& get_account_info_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = get_account_info_response; + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulSignInWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + const std::string& get_account_info_response) { + InitializeSignInWithProviderFakes(get_account_info_response); + provider->SetProviderData(GetFakeOAuthProviderData()); + provider->SetAuthHandler(handler); +} + +void InitializeSuccessfulSignInWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler) { + InitializeSuccessfulSignInWithProviderFlow(provider, handler, + CreateGetAccountInfoFake()); +} + +void InitializeSuccessfulVerifyAssertionFlow( + const std::string& verify_assertion_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = verify_assertion_response; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulVerifyAssertionFlow() { + InitializeSuccessfulVerifyAssertionFlow(CreateVerifyAssertionResponse()); +} + +void SetupAuthDataForPersist(AuthData* auth_data) { + UserData previous_user; + UserData mock_user; + + mock_user.uid = "persist_id"; + mock_user.email = "test@persist.com"; + mock_user.display_name = "persist_name"; + mock_user.photo_url = "persist_photo"; + mock_user.provider_id = "persist_provider"; + mock_user.phone_number = "persist_phone"; + mock_user.is_anonymous = false; + mock_user.is_email_verified = true; + mock_user.id_token = "persist_token"; + mock_user.refresh_token = "persist_refresh_token"; + mock_user.access_token = "persist_access_token"; + mock_user.access_token_expiration_date = 12345; + mock_user.has_email_password_credential = true; + mock_user.last_sign_in_timestamp = 67890; + mock_user.creation_timestamp = 98765; + UserView::ResetUser(auth_data, mock_user, &previous_user); +} + +bool WaitOnLoadPersistence(AuthData* auth_data) { + bool load_finished = false; + int load_wait_counter = 0; + while (!load_finished) { + if (load_wait_counter >= kWaitForLoadMaxTryout) { + break; + } + load_wait_counter++; + firebase::internal::Sleep(1); + { + MutexLock lock(auth_data->listeners_mutex); + load_finished = !auth_data->persistent_cache_load_pending; + } + } + return load_finished; +} + +} // namespace + +class AuthDesktopTest : public ::testing::Test { + protected: + void SetUp() override { + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); + AppOptions options = testing::MockAppOptions(); + options.set_app_id("com.firebase.test"); + options.set_api_key(API_KEY); + firebase_app_ = std::unique_ptr(App::Create(options)); + firebase_auth_ = std::unique_ptr(Auth::GetAuth(firebase_app_.get())); + EXPECT_NE(nullptr, firebase_auth_); + + firebase_auth_->AddIdTokenListener(&id_token_listener); + firebase_auth_->AddAuthStateListener(&auth_state_listener); + + WaitOnLoadPersistence(firebase_auth_->auth_data_); + } + + void TearDown() override { + // Reset listeners before signing out. + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + firebase_auth_->SignOut(); + firebase_auth_.reset(nullptr); + firebase_app_.reset(nullptr); + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + Future ProcessSignInWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + bool trigger_sign_in) { + InitializeSignInWithProviderFakes(CreateGetAccountInfoFake()); + provider->SetProviderData(GetFakeOAuthProviderData()); + provider->SetAuthHandler(handler); + Future future = firebase_auth_->SignInWithProvider(provider); + if (trigger_sign_in) { + handler->TriggerSignInComplete(); + } + return future; + } + + std::unique_ptr firebase_app_; + std::unique_ptr firebase_auth_; + + test::IdTokenChangesCounter id_token_listener; + test::AuthStateChangesCounter auth_state_listener; +}; + +TEST_F(AuthDesktopTest, + TestSignInWithProviderReturnsUnsupportedError) { + FederatedOAuthProvider provider; + Future future = firebase_auth_->SignInWithProvider(&provider); + EXPECT_EQ(future.result()->user, nullptr); + EXPECT_EQ(future.error(), kAuthErrorUnimplemented); + EXPECT_EQ(std::string(future.error_message()), + "Operation is not supported on non-mobile systems."); +} + +TEST_F(AuthDesktopTest, + DISABLED_TestSignInWithProviderAndHandlerPassingIntegrityChecks) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler(/*extra_integrity_checks_=*/true); + + InitializeSuccessfulSignInWithProviderFlow(&provider, &handler); + Future future = firebase_auth_->SignInWithProvider(&provider); + handler.TriggerSignInComplete(); + SignInResult sign_in_result = WaitForFuture(future); +} + +TEST_F(AuthDesktopTest, + DISABLED_TestPendingSignInWithProviderSecondConcurrentSignInFails) { + FederatedOAuthProvider provider1; + OAuthProviderTestHandler handler1; + InitializeSuccessfulSignInWithProviderFlow(&provider1, &handler1); + + FederatedOAuthProvider provider2; + provider2.SetProviderData(GetFakeOAuthProviderData()); + + OAuthProviderTestHandler handler2; + provider2.SetAuthHandler(&handler2); + Future future1 = firebase_auth_->SignInWithProvider(&provider1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = firebase_auth_->SignInWithProvider(&provider2); + VerifySignInResult(future2, kAuthErrorFederatedProviderAreadyInUse); + handler1.TriggerSignInComplete(); + const SignInResult sign_in_result = WaitForFuture(future1); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteSignInResultUserPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + FederatedAuthProvider::AuthenticatedUserData user_data = + *(handler.GetAuthenticatedUserData()); + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + EXPECT_NE(sign_in_result.user, nullptr); + EXPECT_EQ(sign_in_result.user->is_email_verified(), + user_data.is_email_verified); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + EXPECT_EQ(sign_in_result.user->uid(), user_data.uid); + EXPECT_EQ(sign_in_result.user->email(), user_data.email); + EXPECT_EQ(sign_in_result.user->display_name(), user_data.display_name); + EXPECT_EQ(sign_in_result.user->photo_url(), user_data.photo_url); + EXPECT_EQ(sign_in_result.user->provider_id(), user_data.provider_id); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullUIDFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->uid = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullDisplayNamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->display_name = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullUsernamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->user_name = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullPhotoUrlPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->photo_url = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullProvderIdFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->provider_id = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullAccessTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->access_token = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullRefreshTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->refresh_token = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteExpiresInMaxUInt64Passes) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->token_expires_in_seconds = ULONG_MAX; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteErrorMessagePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/false); + const char* error_message = "oh nos!"; + handler.TriggerSignInCompleteWithError(kAuthErrorApiNotAvailable, + error_message); + VerifySignInResult(future, kAuthErrorApiNotAvailable, error_message); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullErrorMessageFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/false); + handler.TriggerSignInCompleteWithError(kAuthErrorApiNotAvailable, nullptr); + VerifySignInResult(future, kAuthErrorApiNotAvailable); +} + +// Test the helper function GetAccountInfo. +TEST_F(AuthDesktopTest, TestGetAccountInfo) { + const auto response = + FakeSuccessfulResponse("GetAccountInfoResponse", + "\"users\": " + " [" + " {" + " \"localId\": \"localid123\"," + " \"displayName\": \"dp name\"," + " \"email\": \"abc@efg\"," + " \"photoUrl\": \"www.photo\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"phoneNumber\": \"519\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\"" + " }" + " ]"); + InitializeConfigWithAFake(GetUrlForApi("APIKEY", "getAccountInfo"), response); + + // getAccountInfo never returns new tokens, and can't change current user. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Call the function and verify results. + AuthData auth_data; + AuthImpl auth; + auth_data.auth_impl = &auth; + auth.api_key = "APIKEY"; + const GetAccountInfoResult result = + GetAccountInfo(auth_data, "fake_access_token"); + EXPECT_TRUE(result.IsValid()); + const UserData& user = result.user(); + EXPECT_EQ("localid123", user.uid); + EXPECT_EQ("abc@efg", user.email); + EXPECT_EQ("dp name", user.display_name); + EXPECT_EQ("www.photo", user.photo_url); + EXPECT_EQ("519", user.phone_number); + EXPECT_FALSE(user.is_email_verified); + EXPECT_TRUE(user.has_email_password_credential); +} + +// Test the helper function CompleteSignIn. Since we do not have the access to +// the private members of Auth, we use SignInAnonymously to test it indirectly. +// Let the REST request failed with 503. +TEST_F(AuthDesktopTest, CompleteSignInWithFailedResponse) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = CreateErrorHttpResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + // Because the API call fails, current user shouldn't have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Call the function and verify results. + const User* const user = + WaitForFuture(firebase_auth_->SignInAnonymously(), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +// Test the helper function CompleteSignIn. Since we do not have the access to +// the private members of Auth, we use SignInAnonymously to test it indirectly. +// Let it failed to get account info. +TEST_F(AuthDesktopTest, CompleteSignInWithGetAccountInfoFailure) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + "\"idToken\": \"idtoken123\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\"," + "\"localId\": \"localid123\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateErrorHttpResponse(); + InitializeConfigWithFakes(fakes); + + // User is not updated until getAccountInfo succeeds; calls to signupNewUser + // and getAccountInfo are considered a single "transaction". So if + // getAccountInfo fails, current user shouldn't have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Call the function and verify results. + const User* const user = + WaitForFuture(firebase_auth_->SignInAnonymously(), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +// Test Auth::SignInAnonymously. +TEST_F(AuthDesktopTest, TestSignInAnonymously) { + FakeSetT fakes; + + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + "\"idToken\": \"idtoken123\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\"," + "\"localId\": \"localid123\""); + + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + FakeSuccessfulResponse("GetAccountInfoResponse", + "\"users\": " + " [" + " {" + " \"localId\": \"localid123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\"" + " }" + " ]"); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const User* const user = WaitForFuture(firebase_auth_->SignInAnonymously()); + EXPECT_TRUE(user->is_anonymous()); + EXPECT_EQ("localid123", user->uid()); + EXPECT_EQ("", user->email()); + EXPECT_EQ("", user->display_name()); + EXPECT_EQ("", user->photo_url()); + EXPECT_EQ("Firebase", user->provider_id()); + EXPECT_EQ("", user->phone_number()); + EXPECT_FALSE(user->is_email_verified()); +} + +// Test Auth::SignInWithEmailAndPassword. +TEST_F(AuthDesktopTest, TestSignInWithEmailAndPassword) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyPassword")] = + FakeSuccessfulResponse("VerifyPasswordResponse", + "\"localId\": \"localid123\"," + "\"email\": \"testsignin@example.com\"," + "\"idToken\": \"idtoken123\"," + "\"registered\": true," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + // Call the function and verify results. + const Future future = firebase_auth_->SignInWithEmailAndPassword( + "testsignin@example.com", "testsignin"); + const User* const user = WaitForFuture(future); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +// Test Auth::CreateUserWithEmailAndPassword. +TEST_F(AuthDesktopTest, TestCreateUserWithEmailAndPassword) { + FakeSetT fakes; + + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + "\"idToken\": \"idtoken123\"," + "\"displayName\": \"\"," + "\"email\": \"testsignin@example.com\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\"," + "\"localId\": \"localid123\""); + + fakes[GetUrlForApi(API_KEY, "verifyPassword")] = + FakeSuccessfulResponse("VerifyPasswordResponse", + "\"localId\": \"localid123\"," + "\"email\": \"testsignin@example.com\"," + "\"idToken\": \"idtoken123\"," + "\"registered\": true," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); + + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Future future = firebase_auth_->CreateUserWithEmailAndPassword( + "testsignin@example.com", "testsignin"); + const User* const user = WaitForFuture(future); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +// Test Auth::SignInWithCustomToken. +TEST_F(AuthDesktopTest, TestSignInWithCustomToken) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyCustomToken")] = + FakeSuccessfulResponse("VerifyCustomTokenResponse", + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"idtoken123\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const User* const user = + WaitForFuture(firebase_auth_->SignInWithCustomToken("fake_custom_token")); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +// Test Auth::TestSignInWithCredential. + +TEST_F(AuthDesktopTest, TestSignInWithCredential_GoogleIdToken) { + InitializeSuccessfulVerifyAssertionFlow(); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const User* const user = + WaitForFuture(firebase_auth_->SignInWithCredential(credential)); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +TEST_F(AuthDesktopTest, TestSignInWithCredential_GoogleAccessToken) { + InitializeSuccessfulVerifyAssertionFlow(); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = + GoogleAuthProvider::GetCredential("", "fake_access_token"); + const User* const user = + WaitForFuture(firebase_auth_->SignInWithCredential(credential)); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +TEST_F(AuthDesktopTest, + TestSignInWithCredential_WithFailedVerifyAssertionResponse) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = CreateErrorHttpResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Credential credential = + GoogleAuthProvider::GetCredential("", "fake_access_token"); + const User* const user = WaitForFuture( + firebase_auth_->SignInWithCredential(credential), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +TEST_F(AuthDesktopTest, + TestSignInWithCredential_WithFailedGetAccountInfoResponse) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = + CreateVerifyAssertionResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateErrorHttpResponse(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Credential credential = + GoogleAuthProvider::GetCredential("", "fake_access_token"); + const User* const user = WaitForFuture( + firebase_auth_->SignInWithCredential(credential), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +TEST_F(AuthDesktopTest, TestSignInWithCredential_NeedsConfirmation) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "verifyAssertion"), + FakeSuccessfulResponse("verifyAssertion", "\"needConfirmation\": true")); + + // needConfirmation is considered an error by the SDK, so current user + // shouldn't have been updated. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_auth_->SignInWithCredential(credential), + kAuthErrorAccountExistsWithDifferentCredentials); +} + +TEST_F(AuthDesktopTest, TestSignInAndRetrieveDataWithCredential_GitHub) { + const auto response = CreateVerifyAssertionResponseWithUserInfo( + "github.com", + "\\\\\"login\\\\\": \\\\\"fake_user_name\\\\\"," + "\\\\\"some_str_key\\\\\": \\\\\"some_value\\\\\"," + "\\\\\"some_num_key\\\\\": 123"); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = + GitHubAuthProvider::GetCredential("fake_access_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_STREQ("github.com", sign_in_result.info.provider_id.c_str()); + EXPECT_STREQ("fake_user_name", sign_in_result.info.user_name.c_str()); + + const auto found_str_value = + sign_in_result.info.profile.find(Variant("some_str_key")); + EXPECT_NE(found_str_value, sign_in_result.info.profile.end()); + EXPECT_STREQ("some_value", found_str_value->second.string_value()); + + const auto found_num_value = + sign_in_result.info.profile.find(Variant("some_num_key")); + EXPECT_NE(found_num_value, sign_in_result.info.profile.end()); + EXPECT_EQ(123, found_num_value->second.int64_value()); +} + +TEST_F(AuthDesktopTest, TestSignInAndRetrieveDataWithCredential_Twitter) { + const auto response = CreateVerifyAssertionResponseWithUserInfo( + "twitter.com", "\\\\\"screen_name\\\\\": \\\\\"fake_user_name\\\\\""); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = TwitterAuthProvider::GetCredential( + "fake_access_token", "fake_oauth_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_EQ("twitter.com", sign_in_result.info.provider_id); + EXPECT_EQ("fake_user_name", sign_in_result.info.user_name); +} + +TEST_F(AuthDesktopTest, + TestSignInAndRetrieveDataWithCredential_NoAdditionalInfo) { + const auto response = + CreateVerifyAssertionResponseWithUserInfo("github.com", ""); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = TwitterAuthProvider::GetCredential( + "fake_access_token", "fake_oauth_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_EQ("github.com", sign_in_result.info.provider_id); + EXPECT_THAT(sign_in_result.info.profile, IsEmpty()); + EXPECT_THAT(sign_in_result.info.user_name, IsEmpty()); +} + +TEST_F(AuthDesktopTest, + TestSignInAndRetrieveDataWithCredential_BadUserNameFormat) { + // Deliberately using a number instead of a string, let's make sure it doesn't + // cause a crash. + const auto response = CreateVerifyAssertionResponseWithUserInfo( + "twitter.com", "\\\\\"screen_name\\\\\": 123"); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = TwitterAuthProvider::GetCredential( + "fake_access_token", "fake_oauth_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_EQ("twitter.com", sign_in_result.info.provider_id); + EXPECT_THAT(sign_in_result.info.user_name, IsEmpty()); +} + +TEST_F(AuthDesktopTest, TestFetchProvidersForEmail) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "createAuthUri"), + FakeSuccessfulResponse("CreateAuthUriResponse", + "\"allProviders\": [" + " \"password\"," + " \"example.com\"" + "]," + "\"registered\": true")); + + // Fetch providers flow shouldn't affect current user in any way. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Auth::FetchProvidersResult result = WaitForFuture( + firebase_auth_->FetchProvidersForEmail("fake_email@example.com")); + EXPECT_EQ(2, result.providers.size()); + EXPECT_EQ("password", result.providers[0]); + EXPECT_EQ("example.com", result.providers[1]); +} + +TEST_F(AuthDesktopTest, TestSendPasswordResetEmail) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "getOobConfirmationCode"), + FakeSuccessfulResponse("GetOobConfirmationCodeResponse", + "\"email\": \"fake_email@example.com\"")); + + // Sending password reset email shouldn't affect current user in any way. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + WaitForFuture( + firebase_auth_->SendPasswordResetEmail("fake_email@example.com")); +} + +TEST(UserViewTest, TestCopyUserView) { + // Construct from UserData. + UserData user1; + user1.uid = "mrsspoon"; + UserView view1(user1); + UserView view3(view1); + UserView view4 = view3; + EXPECT_EQ("mrsspoon", view1.user_data().uid); + EXPECT_EQ("mrsspoon", view3.user_data().uid); + EXPECT_EQ("mrsspoon", view4.user_data().uid); + + // Construct from a UserView. + UserData user2; + user2.uid = "dangerm"; + UserView view2(user2); + EXPECT_EQ("dangerm", view2.user_data().uid); + + // Copy a UserView. + view3 = view2; + EXPECT_EQ("mrsspoon", view1.user_data().uid); + EXPECT_EQ("dangerm", view2.user_data().uid); + EXPECT_EQ("dangerm", view3.user_data().uid); +} + +#if defined(FIREBASE_USE_MOVE_OPERATORS) +TEST(UserViewTest, TestMoveUserView) { + UserData user1; + user1.uid = "mrsspoon"; + UserData user2; + user2.uid = "dangerm"; + UserView view1(user1); + UserView view2(user2); + UserView view3(user2); + UserView view4(std::move(view3)); + EXPECT_EQ("mrsspoon", view1.user_data().uid); + EXPECT_EQ("dangerm", view2.user_data().uid); + EXPECT_EQ("dangerm", view4.user_data().uid); + view2 = std::move(view1); + EXPECT_EQ("mrsspoon", view2.user_data().uid); +} +#endif // defined(defined(FIREBASE_USE_MOVE_OPERATORS) + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/fakes.cc b/auth/tests/desktop/fakes.cc new file mode 100644 index 0000000000..348f97838a --- /dev/null +++ b/auth/tests/desktop/fakes.cc @@ -0,0 +1,118 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/tests/desktop/fakes.h" + +#include "testing/config.h" + +namespace firebase { +namespace auth { +namespace test { + +std::string CreateRawJson(const FakeSetT& fakes) { + std::string raw_json = + "{" + " config:" + " ["; + + for (auto i = fakes.begin(); i != fakes.end(); ++i) { + const std::string url = i->first; + const std::string response = i->second; + raw_json += + " {" + " fake: '" + + url + + "'," + " httpresponse: " + + response + " }"; + auto check_end = i; + ++check_end; + if (check_end != fakes.end()) { + raw_json += ','; + } + } + + raw_json += + " ]" + "}"; + + return raw_json; +} + +void InitializeConfigWithFakes(const FakeSetT& fakes) { + firebase::testing::cppsdk::ConfigSet(CreateRawJson(fakes).c_str()); +} + +void InitializeConfigWithAFake(const std::string& url, + const std::string& fake_response) { + FakeSetT fakes; + fakes[url] = fake_response; + InitializeConfigWithFakes(fakes); +} + +std::string GetUrlForApi(const std::string& api_key, + const std::string& api_method) { + const char* const base_url = + "https://www.googleapis.com/identitytoolkit/v3/" + "relyingparty/"; + return std::string{base_url} + api_method + "?key=" + api_key; +} + +std::string FakeSuccessfulResponse(const std::string& body) { + const std::string head = + "{" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: " + " [" + " '{"; + + const std::string tail = + " }'" + " ]" + "}"; + + return head + body + tail; +} + +std::string FakeSuccessfulResponse(const std::string& kind, + const std::string& body) { + return FakeSuccessfulResponse("\"kind\": \"identitytoolkit#" + kind + "\"," + + body); +} + +std::string CreateErrorHttpResponse(const std::string& error) { + const std::string head = + "{" + " header: ['HTTP/1.1 503 Service Unavailable','Server:mock 101']"; + + std::string body; + if (!error.empty()) { + // clang-format off + body = std::string( + "," + " body: ['{" + " \"error\": {" + " \"message\": \"") + error + "\"" + " }" + " }']"; + // clang-format on + } + + const std::string tail = "}"; + return head + body + tail; +} + +} // namespace test +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/fakes.h b/auth/tests/desktop/fakes.h new file mode 100644 index 0000000000..e84d9ed80c --- /dev/null +++ b/auth/tests/desktop/fakes.h @@ -0,0 +1,64 @@ +// Copyright 2017 Google LLC +// +// 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. + +#ifndef FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_FAKES_H_ +#define FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_FAKES_H_ + +#include +#include + +// A set of helpers to reduce repetitive boilerplate to setup fakes in tests. + +namespace firebase { +namespace auth { +namespace test { + +using FakeSetT = std::unordered_map; + +// Creates a JSON string from the given map of fakes (which assumes a very +// simple format, both keys and values can only be strings). +std::string CreateRawJson(const FakeSetT& fakes); + +// Creates a JSON string from the given map of fakes and initializes Firebase +// testing config with this JSON. +void InitializeConfigWithFakes(const FakeSetT& fakes); + +// Creates JSON dictionary with just a single entry (key = url, value +// = fake_response) and initializes Firebase testing config with this JSON. +void InitializeConfigWithAFake(const std::string& url, + const std::string& fake_response); + +// Returns full URL to make a REST request to Identity Toolkit backend. +std::string GetUrlForApi(const std::string& api_key, + const std::string& api_method); + +// Returns string representation of a successful HTTP response with the given +// body. +std::string FakeSuccessfulResponse(const std::string& body); + +// Returns string representation of a successful HTTP response with the given +// body. Body will also contain an entry to specify the "kind" of response, like +// all Identity Toolkit responses do ("kind": +// "identitytoolkit#"). +std::string FakeSuccessfulResponse(const std::string& kind, + const std::string& body); + +// Returns string representation of a 503 HTTP response. +std::string CreateErrorHttpResponse(const std::string& error = ""); + +} // namespace test +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_FAKES_H_ diff --git a/auth/tests/desktop/rpcs/create_auth_uri_test.cc b/auth/tests/desktop/rpcs/create_auth_uri_test.cc new file mode 100644 index 0000000000..92260d615f --- /dev/null +++ b/auth/tests/desktop/rpcs/create_auth_uri_test.cc @@ -0,0 +1,66 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/create_auth_uri_request.h" +#include "auth/src/desktop/rpcs/create_auth_uri_response.h" + +namespace firebase { +namespace auth { + +// Test CreateAuthUriRequest +TEST(CreateAuthUriTest, TestCreateAuthUriRequest) { + std::unique_ptr app(testing::CreateApp()); + CreateAuthUriRequest request("APIKEY", "email"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "createAuthUri?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " identifier: \"email\",\n" + " continueUri: \"http://localhost\"\n" + "}\n", + request.options().post_fields); +} + +// Test CreateAuthUriResponse +TEST(CreateAuthUriTest, TestCreateAuthUriResponse) { + std::unique_ptr app(testing::CreateApp()); + CreateAuthUriResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#CreateAuthUriResponse\",\n" + " \"allProviders\": [\n" + " \"password\"\n" + " ],\n" + " \"registered\": true,\n" + " \"sessionId\": \"cdefgab\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_THAT(response.providers(), ::testing::ElementsAre("password")); + EXPECT_TRUE(response.registered()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/delete_account_test.cc b/auth/tests/desktop/rpcs/delete_account_test.cc new file mode 100644 index 0000000000..7240f31319 --- /dev/null +++ b/auth/tests/desktop/rpcs/delete_account_test.cc @@ -0,0 +1,57 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/delete_account_request.h" +#include "auth/src/desktop/rpcs/delete_account_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test DeleteAccountRequest +TEST(DeleteAccountTest, TestDeleteAccountRequest) { + std::unique_ptr app(testing::CreateApp()); + DeleteAccountRequest request("APIKEY"); + request.SetIdToken("token"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "deleteAccount?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " idToken: \"token\"\n" + "}\n", + request.options().post_fields); +} + +// Test DeleteAccountResponse +TEST(DeleteAccountTest, TestDeleteAccountResponse) { + std::unique_ptr app(testing::CreateApp()); + DeleteAccountResponse response; + const char body[] = + "{\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/get_account_info_test.cc b/auth/tests/desktop/rpcs/get_account_info_test.cc new file mode 100644 index 0000000000..cd4f241c9a --- /dev/null +++ b/auth/tests/desktop/rpcs/get_account_info_test.cc @@ -0,0 +1,81 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/get_account_info_request.h" +#include "auth/src/desktop/rpcs/get_account_info_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test GetAccountInfoRequest +TEST(GetAccountInfoTest, TestGetAccountInfoRequest) { + std::unique_ptr app(testing::CreateApp()); + GetAccountInfoRequest request("APIKEY", "token"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " idToken: \"token\"\n" + "}\n", + request.options().post_fields); +} + +// Test GetAccountInfoResponse +TEST(GetAccountInfoTest, TestGetAccountInfoResponse) { + std::unique_ptr app(App::Create(AppOptions())); + GetAccountInfoResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#GetAccountInfoResponse\",\n" + " \"users\": [\n" + " {\n" + " \"localId\": \"localid123\",\n" + " \"displayName\": \"dp name\",\n" + " \"email\": \"abc@efg\",\n" + " \"photoUrl\": \"www.photo\",\n" + " \"emailVerified\": false,\n" + " \"passwordHash\": \"abcdefg\",\n" + " \"phoneNumber\": \"519\",\n" + " \"passwordUpdatedAt\": 31415926,\n" + " \"validSince\": \"123\",\n" + " \"lastLoginAt\": \"123\",\n" + " \"createdAt\": \"123\"\n" + " }\n" + " ]\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("localid123", response.local_id()); + EXPECT_EQ("dp name", response.display_name()); + EXPECT_EQ("abc@efg", response.email()); + EXPECT_EQ("www.photo", response.photo_url()); + EXPECT_FALSE(response.email_verified()); + EXPECT_EQ("abcdefg", response.password_hash()); + EXPECT_EQ("519", response.phone_number()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc b/auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc new file mode 100644 index 0000000000..53d465275a --- /dev/null +++ b/auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc @@ -0,0 +1,81 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/get_oob_confirmation_code_request.h" +#include "auth/src/desktop/rpcs/get_oob_confirmation_code_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +typedef GetOobConfirmationCodeRequest RequestT; +typedef GetOobConfirmationCodeResponse ResponseT; + +// Test SetVerifyEmailRequest +TEST(GetOobConfirmationCodeTest, SendVerifyEmailRequest) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateSendEmailVerificationRequest("APIKEY"); + request->SetIdToken("token"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " idToken: \"token\",\n" + " requestType: \"VERIFY_EMAIL\"\n" + "}\n", + request->options().post_fields); +} + +TEST(GetOobConfirmationCodeTest, SendPasswordResetEmailRequest) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateSendPasswordResetEmailRequest("APIKEY", "email"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " email: \"email\",\n" + " requestType: \"PASSWORD_RESET\"\n" + "}\n", + request->options().post_fields); +} + +// Test GetOobConfirmationCodeResponse +TEST(GetOobConfirmationCodeTest, TestGetOobConfirmationCodeResponse) { + std::unique_ptr app(testing::CreateApp()); + ResponseT response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#GetOobConfirmationCodeResponse\",\n" + " \"email\": \"my@email\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/reset_password_test.cc b/auth/tests/desktop/rpcs/reset_password_test.cc new file mode 100644 index 0000000000..5e45de3a22 --- /dev/null +++ b/auth/tests/desktop/rpcs/reset_password_test.cc @@ -0,0 +1,61 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/reset_password_request.h" +#include "auth/src/desktop/rpcs/reset_password_response.h" + +namespace firebase { +namespace auth { + +// Test ResetPasswordRequest +TEST(ResetPasswordTest, TestResetPasswordRequest) { + std::unique_ptr app(testing::CreateApp()); + ResetPasswordRequest request("APIKEY", "oob", "password"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "resetPassword?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " oobCode: \"oob\",\n" + " newPassword: \"password\"\n" + "}\n", + request.options().post_fields); +} + +// Test ResetPasswordResponse +TEST(ResetPasswordTest, TestResetPasswordResponse) { + std::unique_ptr app(testing::CreateApp()); + ResetPasswordResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#ResetPasswordResponse\",\n" + " \"email\": \"abc@email\",\n" + " \"requestType\": \"PASSWORD_RESET\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/secure_token_test.cc b/auth/tests/desktop/rpcs/secure_token_test.cc new file mode 100644 index 0000000000..0a8b552c97 --- /dev/null +++ b/auth/tests/desktop/rpcs/secure_token_test.cc @@ -0,0 +1,68 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/secure_token_request.h" +#include "auth/src/desktop/rpcs/secure_token_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test SignUpNewUserRequest using refresh token +TEST(SecureTokenTest, TestSetRefreshRequest) { + std::unique_ptr app(testing::CreateApp()); + SecureTokenRequest request("APIKEY", "token123"); + EXPECT_EQ("https://securetoken.googleapis.com/v1/token?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " grantType: \"refresh_token\",\n" + " refreshToken: \"token123\"\n" + "}\n", + request.options().post_fields); +} + +// Test SecureTokenResponse +TEST(SecureTokenTest, TestSecureTokenResponse) { + std::unique_ptr app(testing::CreateApp()); + SecureTokenResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"access_token\": \"accesstoken123\",\n" + " \"expires_in\": \"3600\",\n" + " \"token_type\": \"Bearer\",\n" + " \"refresh_token\": \"refreshtoken123\",\n" + " \"id_token\": \"idtoken123\",\n" + " \"user_id\": \"localid123\",\n" + " \"project_id\": \"53101460582\"" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("accesstoken123", response.access_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ(3600, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/set_account_info_test.cc b/auth/tests/desktop/rpcs/set_account_info_test.cc new file mode 100644 index 0000000000..622f427db7 --- /dev/null +++ b/auth/tests/desktop/rpcs/set_account_info_test.cc @@ -0,0 +1,173 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/set_account_info_request.h" +#include "auth/src/desktop/rpcs/set_account_info_response.h" + +namespace firebase { +namespace auth { + +typedef SetAccountInfoRequest RequestT; +typedef SetAccountInfoResponse ResponseT; + +// Test SetAccountInfoRequest +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateEmail) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateUpdateEmailRequest("APIKEY", "fakeemail"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " email: \"fakeemail\",\n" + " returnSecureToken: true,\n" + " idToken: \"token\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdatePassword) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUpdatePasswordRequest("APIKEY", "fakepassword"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " password: \"fakepassword\",\n" + " returnSecureToken: true,\n" + " idToken: \"token\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateProfile_Full) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUpdateProfileRequest("APIKEY", "New Name", "new_url"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " displayName: \"New Name\",\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " photoUrl: \"new_url\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateProfile_Partial) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUpdateProfileRequest("APIKEY", nullptr, "new_url"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " photoUrl: \"new_url\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateProfile_DeleteFields) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateUpdateProfileRequest("APIKEY", "", ""); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " deleteAttribute: [\n" + " \"DISPLAY_NAME\",\n" + " \"PHOTO_URL\"\n" + " ]\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, + TestSetAccountInfoRequest_UpdateProfile_DeleteAndUpdate) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateUpdateProfileRequest("APIKEY", "", "new_url"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " photoUrl: \"new_url\",\n" + " deleteAttribute: [\n" + " \"DISPLAY_NAME\"\n" + " ]\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_Unlink) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUnlinkProviderRequest("APIKEY", "fakeprovider"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " deleteProvider: [\n" + " \"fakeprovider\"\n" + " ]\n" + "}\n", + request->options().post_fields); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/sign_up_new_user_test.cc b/auth/tests/desktop/rpcs/sign_up_new_user_test.cc new file mode 100644 index 0000000000..8020349821 --- /dev/null +++ b/auth/tests/desktop/rpcs/sign_up_new_user_test.cc @@ -0,0 +1,110 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_request.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_response.h" + +namespace firebase { +namespace auth { + +// Test SignUpNewUserRequest for making anonymous signin +TEST(SignUpNewUserTest, TestAnonymousSignInRequest) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserRequest request("APIKEY"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true\n" + "}\n", + request.options().post_fields); +} + +// Test SignUpNewUserRequest for using password signin +TEST(SignUpNewUserTest, TestEmailPasswordSignInRequest) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserRequest request("APIKEY", "e@mail", "pwd", "rabbit"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " email: \"e@mail\",\n" + " password: \"pwd\",\n" + " displayName: \"rabbit\",\n" + " returnSecureToken: true\n" + "}\n", + request.options().post_fields); +} + +// Test SignUpNewUserResponse +TEST(SignUpNewUserTest, TestSignUpNewUserResponse) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#SignupNewUserResponse\",\n" + " \"idToken\": \"idtoken123\",\n" + " \"refreshToken\": \"refreshtoken123\",\n" + " \"expiresIn\": \"3600\",\n" + " \"localId\": \"localid123\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); + EXPECT_EQ(3600, response.expires_in()); +} + +TEST(SignUpNewUserTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"OPERATION_NOT_ALLOWED\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorOperationNotAllowed, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ(0, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/test_util.cc b/auth/tests/desktop/rpcs/test_util.cc new file mode 100644 index 0000000000..16caf880b2 --- /dev/null +++ b/auth/tests/desktop/rpcs/test_util.cc @@ -0,0 +1,69 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_request.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_response.h" + +namespace firebase { +namespace auth { + +bool GetNewUserLocalIdAndIdToken(const char* const api_key, + std::string* local_id, + std::string* id_token) { + SignUpNewUserRequest request(api_key); + SignUpNewUserResponse response; + + firebase::rest::CreateTransport()->Perform(request, &response); + + if (response.status() != 200) { + return false; + } + + *local_id = response.local_id(); + *id_token = response.id_token(); + return true; +} + +bool GetNewUserLocalIdAndRefreshToken(const char* const api_key, + std::string* local_id, + std::string* refresh_token) { + SignUpNewUserRequest request(api_key); + SignUpNewUserResponse response; + + firebase::rest::CreateTransport()->Perform(request, &response); + + if (response.status() != 200) { + return false; + } + + *local_id = response.local_id(); + *refresh_token = response.refresh_token(); + return true; +} + +std::string SignUpNewUserAndGetIdToken(const char* const api_key, + const char* const email) { + SignUpNewUserRequest request(api_key, email, "fake_password", ""); + SignUpNewUserResponse response; + + firebase::rest::CreateTransport()->Perform(request, &response); + if (response.status() != 200) { + return ""; + } + return response.id_token(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/test_util.h b/auth/tests/desktop/rpcs/test_util.h new file mode 100644 index 0000000000..62abe172f1 --- /dev/null +++ b/auth/tests/desktop/rpcs/test_util.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#ifndef FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_RPCS_TEST_UTIL_H_ +#define FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_RPCS_TEST_UTIL_H_ + +#include + +namespace firebase { +namespace auth { + +// Sign in a new user and return its local ID and ID token. +bool GetNewUserLocalIdAndIdToken(const char* api_key, std::string* local_id, + std::string* id_token); + +// Sign in a new user and return its local ID and refresh token. +bool GetNewUserLocalIdAndRefreshToken(const char* api_key, + std::string* local_id, + std::string* refresh_token); +std::string SignUpNewUserAndGetIdToken(const char* api_key, + const char* email); + +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_RPCS_TEST_UTIL_H_ diff --git a/auth/tests/desktop/rpcs/verify_assertion_test.cc b/auth/tests/desktop/rpcs/verify_assertion_test.cc new file mode 100644 index 0000000000..e4dc6bc73e --- /dev/null +++ b/auth/tests/desktop/rpcs/verify_assertion_test.cc @@ -0,0 +1,87 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/verify_assertion_request.h" +#include "auth/src/desktop/rpcs/verify_assertion_response.h" + +namespace { +void CheckUrl(const firebase::auth::VerifyAssertionRequest& request) { + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyAssertion?key=APIKEY", + request.options().url); +} +} // namespace + +namespace firebase { +namespace auth { + +// Test VerifyAssertionRequest +TEST(VerifyAssertionTest, TestVerifyAssertionRequest_FromIdToken) { + std::unique_ptr app(testing::CreateApp()); + auto request = + VerifyAssertionRequest::FromIdToken("APIKEY", "provider", "id_token"); + CheckUrl(*request); +} + +TEST(VerifyAssertionTest, TestVerifyAssertionRequest_FromAccessToken) { + std::unique_ptr app(testing::CreateApp()); + auto request = VerifyAssertionRequest::FromAccessToken("APIKEY", "provider", + "access_token"); + CheckUrl(*request); +} + +TEST(VerifyAssertionTest, TestVerifyAssertionRequest_FromAccessTokenAndSecret) { + std::unique_ptr app(testing::CreateApp()); + auto request = VerifyAssertionRequest::FromAccessTokenAndOAuthSecret( + "APIKEY", "provider", "access_token", "oauth_secret"); + CheckUrl(*request); +} + +TEST(VerifyAssertionTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyAssertionResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"INVALID_IDP_RESPONSE\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorInvalidCredential, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ(0, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/verify_custom_token_test.cc b/auth/tests/desktop/rpcs/verify_custom_token_test.cc new file mode 100644 index 0000000000..3e13b552e1 --- /dev/null +++ b/auth/tests/desktop/rpcs/verify_custom_token_test.cc @@ -0,0 +1,90 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/verify_custom_token_request.h" +#include "auth/src/desktop/rpcs/verify_custom_token_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test VerifyCustomTokenRequest +TEST(VerifyCustomTokenTest, TestVerifyCustomTokenRequest) { + std::unique_ptr app(testing::CreateApp()); + VerifyCustomTokenRequest request("APIKEY", "token123"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyCustomToken?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " token: \"token123\"\n" + "}\n", + request.options().post_fields); +} + +// Test VerifyCustomTokenResponse +TEST(VerifyCustomTokenTest, TestVerifyCustomTokenResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyCustomTokenResponse response; + // An example HTTP response JSON. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#VerifyCustomTokenResponse\",\n" + " \"idToken\": \"idtoken123\",\n" + " \"refreshToken\": \"refreshtoken123\",\n" + " \"expiresIn\": \"3600\",\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); +} + +TEST(VerifyCustomTokenTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyCustomTokenResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"CREDENTIAL_MISMATCH\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorCustomTokenMismatch, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ(0, response.expires_in()); +} +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/verify_password_test.cc b/auth/tests/desktop/rpcs/verify_password_test.cc new file mode 100644 index 0000000000..04fecad20d --- /dev/null +++ b/auth/tests/desktop/rpcs/verify_password_test.cc @@ -0,0 +1,105 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/verify_password_request.h" +#include "auth/src/desktop/rpcs/verify_password_response.h" + +namespace firebase { +namespace auth { + +// Test VerifyPasswordRequest +TEST(VerifyPasswordTest, TestVerifyPasswordRequest) { + std::unique_ptr app(testing::CreateApp()); + VerifyPasswordRequest request("APIKEY", "abc@email", "pwd"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " email: \"abc@email\",\n" + " password: \"pwd\",\n" + " returnSecureToken: true\n" + "}\n", + request.options().post_fields); +} + +// Test VerifyPasswordResponse +TEST(VerifyPasswordTest, TestVerifyPasswordResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyPasswordResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#VerifyPasswordResponse\",\n" + " \"localId\": \"localid123\",\n" + " \"email\": \"abc@email\",\n" + " \"displayName\": \"ABC\",\n" + " \"idToken\": \"idtoken123\",\n" + " \"registered\": true,\n" + " \"refreshToken\": \"refreshtoken123\",\n" + " \"expiresIn\": \"3600\",\n" + " \"photoUrl\": \"dp.google\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("localid123", response.local_id()); + EXPECT_EQ("abc@email", response.email()); + EXPECT_EQ("ABC", response.display_name()); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); + EXPECT_EQ("dp.google", response.photo_url()); + EXPECT_EQ(3600, response.expires_in()); +} + +TEST(VerifyPasswordTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyPasswordResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"WEAK_PASSWORD\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorWeakPassword, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.email()); + EXPECT_EQ("", response.display_name()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ("", response.photo_url()); + EXPECT_EQ(0, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/test_utils.cc b/auth/tests/desktop/test_utils.cc new file mode 100644 index 0000000000..de86beed5a --- /dev/null +++ b/auth/tests/desktop/test_utils.cc @@ -0,0 +1,71 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/tests/desktop/test_utils.h" + +namespace firebase { +namespace auth { +namespace test { + +namespace detail { +ListenerChangeCounter::ListenerChangeCounter() + : actual_changes_(0), expected_changes_(-1) {} + +ListenerChangeCounter::~ListenerChangeCounter() { Verify(); } + +void ListenerChangeCounter::ExpectChanges(const int num) { + expected_changes_ = num; +} +void ListenerChangeCounter::VerifyAndReset() { + Verify(); + expected_changes_ = -1; + actual_changes_ = 0; +} + +void ListenerChangeCounter::Verify() { + if (expected_changes_ != -1) { + EXPECT_EQ(expected_changes_, actual_changes_); + } +} + +} // namespace detail + +void IdTokenChangesCounter::OnIdTokenChanged(Auth* const /*unused*/) { + ++actual_changes_; +} + +void AuthStateChangesCounter::OnAuthStateChanged(Auth* const /*unused*/) { + ++actual_changes_; +} + +using ::testing::NotNull; +using ::testing::StrNe; + +void WaitForFuture(const firebase::Future& future, + const firebase::auth::AuthError expected_error) { + while (future.status() == firebase::kFutureStatusPending) { + } + [&] { + ASSERT_EQ(firebase::kFutureStatusComplete, future.status()); + EXPECT_EQ(expected_error, future.error()); + if (expected_error != kAuthErrorNone) { + EXPECT_THAT(future.error_message(), NotNull()); + EXPECT_THAT(future.error_message(), StrNe("")); + } + }(); +} + +} // namespace test +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/test_utils.h b/auth/tests/desktop/test_utils.h new file mode 100644 index 0000000000..2677abdbec --- /dev/null +++ b/auth/tests/desktop/test_utils.h @@ -0,0 +1,295 @@ +// Copyright 2017 Google LLC +// +// 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. + +#ifndef FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_TEST_UTILS_H_ +#define FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_TEST_UTILS_H_ + +#include "app/src/include/firebase/future.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/auth_desktop.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/types.h" + +namespace firebase { +namespace auth { +namespace test { + +namespace detail { +// Base class to test how many times a listener was called. +// Register one of the implementations below with the Auth class +// (IdToken/AuthStateChangesCounter), then call ExpectChanges(number) on it. By +// default, the check will be done in the destructor, but you can call +// VerifyAndReset to force the check while the test is still running, which is +// useful if the test involves several sign in operations. +class ListenerChangeCounter { + public: + ListenerChangeCounter(); + virtual ~ListenerChangeCounter(); + + void ExpectChanges(int num); + void VerifyAndReset(); + + protected: + int actual_changes_; + + private: + void Verify(); + + int expected_changes_; +}; +} // namespace detail + +inline FederatedAuthProvider::AuthenticatedUserData +GetFakeAuthenticatedUserData() { + FederatedAuthProvider::AuthenticatedUserData user_data; + user_data.uid = "localid123"; + user_data.email = "testsignin@example.com"; + user_data.display_name = ""; + user_data.photo_url = ""; + user_data.provider_id = "Firebase"; + user_data.is_email_verified = false; + user_data.raw_user_info["login"] = Variant("test_login@example.com"); + user_data.raw_user_info["screen_name"] = Variant("test_screen_name"); + user_data.access_token = "12345ABC"; + user_data.refresh_token = "67890DEF"; + user_data.token_expires_in_seconds = 60; + return user_data; +} + +inline void VerifySignInResult(const Future& future, + AuthError auth_error, + const char* error_message) { + EXPECT_EQ(future.status(), kFutureStatusComplete); + EXPECT_EQ(future.error(), auth_error); + if (error_message != nullptr) { + EXPECT_STREQ(future.error_message(), error_message); + } +} + +inline void VerifySignInResult(const Future& future, + AuthError auth_error) { + VerifySignInResult(future, auth_error, + /*error_message=*/nullptr); + EXPECT_EQ(future.error(), auth_error); +} + +inline FederatedOAuthProviderData GetFakeOAuthProviderData() { + FederatedOAuthProviderData provider_data; + provider_data.provider_id = + firebase::auth::GitHubAuthProvider::kProviderId; + provider_data.scopes = {"read:user", "user:email"}; + provider_data.custom_parameters = {{"req_id", "1234"}}; + return provider_data; +} + +// OAuthProviderHandler to orchestrate Auth::SignInWithProvider, +// User::LinkWithProvider and User::ReauthenticateWithProver tests. Provides +// a mechanism to test the callback surface of the FederatedAuthProvider. +// Additionally the class provides option checks (extra_integrity_checks) to +// ensure the validity of the data that the Auth implementation passes +// to the handler, such as a non-null auth completion handle. +class OAuthProviderTestHandler + : public FederatedAuthProvider::Handler { + public: + explicit OAuthProviderTestHandler(bool extra_integrity_checks = false) { + extra_integrity_checks_ = extra_integrity_checks; + authenticated_user_data_ = GetFakeAuthenticatedUserData(); + sign_in_auth_completion_handle_ = nullptr; + link_auth_completion_handle_ = nullptr; + reauthenticate_auth_completion_handle_ = nullptr; + } + + explicit OAuthProviderTestHandler( + const FederatedAuthProvider::AuthenticatedUserData& + authenticated_user_data, + bool extra_integrity_checks = false) { + extra_integrity_checks_ = extra_integrity_checks; + authenticated_user_data_ = authenticated_user_data; + sign_in_auth_completion_handle_ = nullptr; + } + + void SetAuthenticatedUserData( + const FederatedAuthProvider::AuthenticatedUserData& user_data) { + authenticated_user_data_ = user_data; + } + + FederatedAuthProvider::AuthenticatedUserData* GetAuthenticatedUserData() { + return &authenticated_user_data_; + } + + // Caches the auth_completion_handler, which will be invoked via + // the test framework's inovcation of the TriggerSignInComplete method. + void OnSignIn(const FederatedOAuthProviderData& provider_data, + AuthCompletionHandle* completion_handle) override { + // ensure we're not invoking this handler twice, thereby overwritting the + // sign_in_auth_completion_handle_ + assert(sign_in_auth_completion_handle_ == nullptr); + sign_in_auth_completion_handle_ = completion_handle; + PerformIntegrityChecks(provider_data, completion_handle); + } + + // Invokes SignInComplete with the auth completion handler provided to this + // during the Auth::SignInWithProvider flow. The ability to trigger this from + // the test framework, instead of immediately from OnSignIn, provides + // mechanisms to test multiple on-going authentication/sign-in requests on + // the Auth object. + void TriggerSignInComplete() { + assert(sign_in_auth_completion_handle_); + SignInComplete(sign_in_auth_completion_handle_, authenticated_user_data_, + /*auth_error=*/kAuthErrorNone, ""); + } + + // Invokes SignInComplete with specific auth error codes and error messages. + void TriggerSignInCompleteWithError(AuthError auth_error, + const char* error_message) { + assert(sign_in_auth_completion_handle_); + SignInComplete(sign_in_auth_completion_handle_, authenticated_user_data_, + auth_error, error_message); + } + + // Caches the auth_completion_handler, which will be invoked via + // the test framework's inovcation of the TriggerLinkComplete method. + void OnLink(const FederatedOAuthProviderData& provider_data, + AuthCompletionHandle* completion_handle) override { + assert(link_auth_completion_handle_ == nullptr); + link_auth_completion_handle_ = completion_handle; + PerformIntegrityChecks(provider_data, completion_handle); + } + + // Invokes LinkComplete with the auth completion handler provided to this + // during the User::LinkWithProvider flow. The ability to trigger this from + // the test framework, instead of immediately from OnLink, provides + // mechanisms to test multiple on-going authentication/link requests on + // the User object. + void TriggerLinkComplete() { + assert(link_auth_completion_handle_); + LinkComplete(link_auth_completion_handle_, authenticated_user_data_, + /*auth_error=*/kAuthErrorNone, ""); + } + + // Invokes Link Complete with a specific auth error code and error message + void TriggerLinkCompleteWithError(AuthError auth_error, + const char* error_message) { + assert(link_auth_completion_handle_); + LinkComplete(link_auth_completion_handle_, authenticated_user_data_, + auth_error, error_message); + } + + // Caches the auth_completion_handler, which will be invoked via + // the test framework's inovcation of the TriggerReauthenticateComplete + // method. + void OnReauthenticate(const FederatedOAuthProviderData& provider_data, + AuthCompletionHandle* completion_handle) override { + assert(reauthenticate_auth_completion_handle_ == nullptr); + reauthenticate_auth_completion_handle_ = completion_handle; + PerformIntegrityChecks(provider_data, completion_handle); + } + + // Invokes ReauthenticateComplete with the auth completion handler provided to + // this during the User::ReauthenticateWithProvider flow. The ability to + // trigger this from the test framework, instead of immediately from + // OnReauthneticate, provides mechanisms to test multiple on-going + // re-authentication requests on the User object. + void TriggerReauthenticateComplete() { + assert(reauthenticate_auth_completion_handle_); + ReauthenticateComplete(reauthenticate_auth_completion_handle_, + authenticated_user_data_, + /*auth_error=*/kAuthErrorNone, ""); + } + + // Invokes ReauthenticateComplete with a specific auth error code and error + // message + void TriggerReauthenticateCompleteWithError(AuthError auth_error, + const char* error_message) { + assert(reauthenticate_auth_completion_handle_); + ReauthenticateComplete(reauthenticate_auth_completion_handle_, + authenticated_user_data_, auth_error, error_message); + } + + private: + void PerformIntegrityChecks(const FederatedOAuthProviderData& provider_data, + const AuthCompletionHandle* completion_handle) { + if (extra_integrity_checks_) { + // check the auth_completion_handle the implementation provided. + // note that the auth completion handle is an opaque type for our users, + // and normal applications wouldn't get a chance to do these sorts of + // checks. + EXPECT_NE(completion_handle, nullptr); + + // ensure that the auth data object has been configured in the handle. + assert(completion_handle->auth_data); + EXPECT_EQ(completion_handle->auth_data->future_impl.GetFutureStatus( + completion_handle->future_handle.get()), + kFutureStatusPending); + FederatedOAuthProviderData expected_provider_data = + GetFakeOAuthProviderData(); + EXPECT_EQ(provider_data.provider_id, expected_provider_data.provider_id); + EXPECT_EQ(provider_data.scopes, expected_provider_data.scopes); + EXPECT_EQ(provider_data.custom_parameters, + expected_provider_data.custom_parameters); + } + } + + AuthCompletionHandle* sign_in_auth_completion_handle_; + AuthCompletionHandle* link_auth_completion_handle_; + AuthCompletionHandle* reauthenticate_auth_completion_handle_; + FederatedAuthProvider::AuthenticatedUserData authenticated_user_data_; + bool extra_integrity_checks_; +}; + +class IdTokenChangesCounter : public detail::ListenerChangeCounter, + public IdTokenListener { + public: + void OnIdTokenChanged(Auth* /*unused*/) override; +}; + +class AuthStateChangesCounter : public detail::ListenerChangeCounter, + public AuthStateListener { + public: + void OnAuthStateChanged(Auth* /*unused*/) override; +}; + +// Waits until the given future is complete and asserts that it completed with +// the given error (no error by default). Returns the future's result. +template +T WaitForFuture(const firebase::Future& future, + const firebase::auth::AuthError expected_error = + firebase::auth::kAuthErrorNone) { + while (future.status() == firebase::kFutureStatusPending) { + } + // This is wrapped in a lambda to work around the assertion macro expecting + // the function to return void. + [&] { + ASSERT_EQ(firebase::kFutureStatusComplete, future.status()); + EXPECT_EQ(expected_error, future.error()); + if (expected_error != kAuthErrorNone) { + EXPECT_THAT(future.error_message(), ::testing::NotNull()); + EXPECT_THAT(future.error_message(), ::testing::StrNe("")); + } + }(); + return *future.result(); +} + +// Waits until the given future is complete and asserts that it completed with +// the given error (no error by default). +void WaitForFuture( + const firebase::Future& future, + firebase::auth::AuthError expected_error = firebase::auth::kAuthErrorNone); + +} // namespace test +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_TEST_UTILS_H_ diff --git a/auth/tests/desktop/user_desktop_test.cc b/auth/tests/desktop/user_desktop_test.cc new file mode 100644 index 0000000000..a0bd7c1377 --- /dev/null +++ b/auth/tests/desktop/user_desktop_test.cc @@ -0,0 +1,1217 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/src/desktop/user_desktop.h" + +#include "app/rest/transport_builder.h" +#include "app/rest/transport_curl.h" +#include "app/rest/transport_mock.h" +#include "app/src/include/firebase/app.h" +#include "app/src/mutex.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/auth_desktop.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/user.h" +#include "auth/tests/desktop/fakes.h" +#include "auth/tests/desktop/test_utils.h" +#include "testing/config.h" +#include "testing/ticker.h" +#include "flatbuffers/stl_emulation.h" + +namespace firebase { +namespace auth { + +using test::CreateErrorHttpResponse; +using test::FakeSetT; +using test::FakeSuccessfulResponse; +using test::GetFakeOAuthProviderData; +using test::GetUrlForApi; +using test::InitializeConfigWithAFake; +using test::InitializeConfigWithFakes; +using test::OAuthProviderTestHandler; +using test::VerifySignInResult; +using test::WaitForFuture; + +using ::testing::AnyOf; +using ::testing::IsEmpty; + +namespace { + +const char* const API_KEY = "MY-FAKE-API-KEY"; +// Constant, describing how many times we would like to sleep 1ms to wait +// for loading persistence cache. +const int kWaitForLoadMaxTryout = 500; + +void InitializeSignUpFlowFakes() { + FakeSetT fakes; + + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + " \"idToken\": \"idtoken123\"," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"," + " \"localId\": \"localid123\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + FakeSuccessfulResponse("GetAccountInfoResponse", + " \"users\": [" + " {" + " \"localId\": \"localid123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"456\"" + " }" + " ]"); + + InitializeConfigWithFakes(fakes); +} + +std::string GetSingleFakeProvider(const std::string& provider_id) { + // clang-format off + return std::string( + " {" + " \"federatedId\": \"fake_uid\"," + " \"email\": \"fake_email@example.com\"," + " \"displayName\": \"fake_display_name\"," + " \"photoUrl\": \"fake_photo_url\"," + " \"providerId\": \"") + provider_id + "\"," + " \"phoneNumber\": \"123123\"" + " }"; + // clang-format on +} + +std::string GetFakeProviderInfo( + const std::string& provider_id = "fake_provider_id") { + return std::string("\"providerUserInfo\": [") + + GetSingleFakeProvider(provider_id) + "]"; +} + +std::string FakeSetAccountInfoResponse() { + return FakeSuccessfulResponse( + "SetAccountInfoResponse", + std::string("\"localId\": \"fake_local_id\"," + "\"email\": \"new_fake_email@example.com\"," + "\"idToken\": \"new_fake_token\"," + "\"expiresIn\": \"3600\"," + "\"passwordHash\": \"new_fake_hash\"," + "\"emailVerified\": false,") + + GetFakeProviderInfo()); +} + +std::string FakeSetAccountInfoResponseWithDetails() { + return FakeSuccessfulResponse( + "SetAccountInfoResponse", + std::string("\"localId\": \"fake_local_id\"," + "\"email\": \"new_fake_email@example.com\"," + "\"idToken\": \"new_fake_token2\"," + "\"expiresIn\": \"3600\"," + "\"passwordHash\": \"new_fake_hash\"," + "\"displayName\": \"Fake Name\"," + "\"photoUrl\": \"https://fake_url.com\"," + "\"emailVerified\": false,") + + GetFakeProviderInfo()); +} + +std::string FakeVerifyAssertionResponse() { + return FakeSuccessfulResponse("VerifyAssertionResponse", + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"verify_idtoken123\"," + "\"providerId\": \"google.com\"," + "\"refreshToken\": \"verify_refreshtoken123\"," + "\"expiresIn\": \"3600\""); +} + +std::string FakeGetAccountInfoResponse() { + return FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\",") + + GetFakeProviderInfo() + + " }" + " ]"); +} + +std::string CreateGetAccountInfoFake() { + return FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\",") + + GetFakeProviderInfo() + + " }" + " ]"); +} + +void InitializeAuthorizeWithProviderFakes( + const std::string& get_account_info_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = get_account_info_response; + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulAuthenticateWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + const std::string& get_account_info_response) { + InitializeAuthorizeWithProviderFakes(get_account_info_response); + provider->SetProviderData(GetFakeOAuthProviderData()); + provider->SetAuthHandler(handler); +} + +void InitializeSuccessfulAuthenticateWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler) { + InitializeSuccessfulAuthenticateWithProviderFlow(provider, handler, + CreateGetAccountInfoFake()); +} + +void VerifyUser(const User& user) { + EXPECT_EQ("localid123", user.uid()); + EXPECT_EQ("testsignin@example.com", user.email()); + EXPECT_EQ("", user.display_name()); + EXPECT_EQ("", user.photo_url()); + EXPECT_EQ("Firebase", user.provider_id()); + EXPECT_EQ("", user.phone_number()); + EXPECT_FALSE(user.is_email_verified()); +} + +void VerifyProviderData(const User& user) { + const std::vector& provider_data = user.provider_data(); + EXPECT_EQ(1, provider_data.size()); + if (provider_data.empty()) { + return; // Avoid crashing on vector out-of-bounds access below + } + EXPECT_EQ("fake_uid", provider_data[0]->uid()); + EXPECT_EQ("fake_email@example.com", provider_data[0]->email()); + EXPECT_EQ("fake_display_name", provider_data[0]->display_name()); + EXPECT_EQ("fake_photo_url", provider_data[0]->photo_url()); + EXPECT_EQ("fake_provider_id", provider_data[0]->provider_id()); + EXPECT_EQ("123123", provider_data[0]->phone_number()); +} + +void InitializeSuccessfulVerifyAssertionFlow( + const std::string& verify_assertion_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = verify_assertion_response; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = FakeGetAccountInfoResponse(); + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulVerifyAssertionFlow() { + InitializeSuccessfulVerifyAssertionFlow(FakeVerifyAssertionResponse()); +} + +bool WaitOnLoadPersistence(AuthData* auth_data) { + bool load_finished = false; + int load_wait_counter = 0; + while (!load_finished) { + if (load_wait_counter >= kWaitForLoadMaxTryout) { + break; + } + load_wait_counter++; + firebase::internal::Sleep(1); + { + MutexLock lock(auth_data->listeners_mutex); + load_finished = !auth_data->persistent_cache_load_pending; + } + } + return load_finished; +} + +} // namespace + +class UserDesktopTest : public ::testing::Test { + protected: + UserDesktopTest() : sem_(0) {} + + void SetUp() override { + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); + AppOptions options = testing::MockAppOptions(); + options.set_api_key(API_KEY); + firebase_app_ = std::unique_ptr(testing::CreateApp(options)); + firebase_auth_ = std::unique_ptr(Auth::GetAuth(firebase_app_.get())); + + InitializeSignUpFlowFakes(); + + firebase_auth_->AddIdTokenListener(&id_token_listener); + firebase_auth_->AddAuthStateListener(&auth_state_listener); + + WaitOnLoadPersistence(firebase_auth_->auth_data_); + + // Current user should be updated upon successful anonymous sign-in. + // Should expect one extra trigger during either listener add after load + // credential is done, or load finish after listener added, so changed + // twice. + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + Future future = firebase_auth_->SignInAnonymously(); + while (future.status() == kFutureStatusPending) { + } + firebase_user_ = firebase_auth_->current_user(); + EXPECT_NE(nullptr, firebase_user_); + + // Reset listeners before tests are run. + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + } + + void TearDown() override { + // Reset listeners before signing out. + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + firebase_auth_->SignOut(); + firebase_auth_.reset(nullptr); + firebase_app_.reset(nullptr); + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + Future ProcessLinkWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + bool trigger_link) { + InitializeSuccessfulAuthenticateWithProviderFlow(provider, handler); + Future future = firebase_user_->LinkWithProvider(provider); + if (trigger_link) { + handler->TriggerLinkComplete(); + } + return future; + } + + Future ProcessReauthenticateWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + bool trigger_reauthenticate) { + InitializeSuccessfulAuthenticateWithProviderFlow(provider, handler); + Future future = + firebase_user_->ReauthenticateWithProvider(provider); + if (trigger_reauthenticate) { + handler->TriggerReauthenticateComplete(); + } + return future; + } + + std::unique_ptr firebase_app_; + std::unique_ptr firebase_auth_; + User* firebase_user_ = nullptr; + + test::IdTokenChangesCounter id_token_listener; + test::AuthStateChangesCounter auth_state_listener; + + Semaphore sem_; +}; + +// Test that metadata is correctly being populated and exposed +TEST_F(UserDesktopTest, TestAccountMetadata) { + EXPECT_EQ(123, + firebase_auth_->current_user()->metadata().last_sign_in_timestamp); + EXPECT_EQ(456, firebase_auth_->current_user()->metadata().creation_timestamp); +} + +TEST_F(UserDesktopTest, TestGetToken) { + const auto api_url = + std::string("https://securetoken.googleapis.com/v1/token?key=") + API_KEY; + InitializeConfigWithAFake( + api_url, + FakeSuccessfulResponse("\"access_token\": \"new accesstoken123\"," + "\"expires_in\": \"3600\"," + "\"token_type\": \"Bearer\"," + "\"refresh_token\": \"new refreshtoken123\"," + "\"id_token\": \"new idtoken123\"," + "\"user_id\": \"localid123\"," + "\"project_id\": \"53101460582\"")); + + // Token should change, but user stays the same. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + // Call the function and verify results. + std::string token = WaitForFuture(firebase_user_->GetToken(false)); + EXPECT_EQ("idtoken123", token); + + // Call again won't change token since it is still valid. + token = WaitForFuture(firebase_user_->GetToken(false)); + EXPECT_NE("new idtoken123", token); + + // Call again to force refreshing token. + const std::string new_token = WaitForFuture(firebase_user_->GetToken(true)); + EXPECT_NE(token, new_token); + EXPECT_EQ("new idtoken123", new_token); +} + +TEST_F(UserDesktopTest, TestDelete) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "deleteAccount"), + FakeSuccessfulResponse("DeleteAccountResponse", "")); + + // Expect logout. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + EXPECT_FALSE(firebase_user_->uid().empty()); + WaitForFuture(firebase_user_->Delete()); + EXPECT_TRUE(firebase_user_->uid().empty()); +} + +TEST_F(UserDesktopTest, TestSendEmailVerification) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "getOobConfirmationCode"), + FakeSuccessfulResponse("GetOobConfirmationCodeResponse", + "\"email\": \"fake_email@example.com\"")); + + // Sending email shouldn't affect the current user in any way. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->SendEmailVerification()); +} + +TEST_F(UserDesktopTest, TestReload) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "getAccountInfo"), + FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\": [" + " {" + " \"localId\": \"fake_local_id\"," + " \"email\": \"fake_email@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"fake_hash\"," + " \"passwordUpdatedAt\": 1.509402565E12," + // Note: these values are copied from an actual + // backend response, so it seems that backend uses + // seconds for validSince but microseconds for the + // other time fields. + " \"validSince\": \"1509402565\"," + " \"lastLoginAt\": \"1509402565000\"," + " \"createdAt\": \"1509402565000\",") + + GetFakeProviderInfo() + + " }" + "]")); + + // User stayed the same, and GetAccountInfoResponse doesn't contain tokens. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->Reload()); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of setting a new email on the currently logged in user. +TEST_F(UserDesktopTest, TestUpdateEmail) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const std::string new_email = "new_fake_email@example.com"; + + EXPECT_NE(new_email, firebase_user_->email()); + WaitForFuture(firebase_user_->UpdateEmail(new_email.c_str())); + EXPECT_EQ(new_email, firebase_user_->email()); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of setting a new password on the currently logged in +// user. +TEST_F(UserDesktopTest, TestUpdatePassword) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->UpdatePassword("new_password")); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of setting new profile properties (display name and +// photo URL) on the currently logged in user. +TEST_F(UserDesktopTest, TestUpdateProfile_Update) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponseWithDetails()); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const std::string display_name = "Fake Name"; + const std::string photo_url = "https://fake_url.com"; + User::UserProfile profile; + profile.display_name = display_name.c_str(); + profile.photo_url = photo_url.c_str(); + + EXPECT_NE(display_name, firebase_user_->display_name()); + EXPECT_NE(photo_url, firebase_user_->photo_url()); + WaitForFuture(firebase_user_->UpdateUserProfile(profile)); + EXPECT_EQ(display_name, firebase_user_->display_name()); + EXPECT_EQ(photo_url, firebase_user_->photo_url()); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of deleting profile properties from the currently logged +// in user (setting display name and photo URL to be blank). +TEST_F(UserDesktopTest, TestUpdateProfile_Delete) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponseWithDetails()); + + const std::string display_name = "Fake Name"; + const std::string photo_url = "https://fake_url.com"; + User::UserProfile profile; + profile.display_name = display_name.c_str(); + profile.photo_url = photo_url.c_str(); + + WaitForFuture(firebase_user_->UpdateUserProfile(profile)); + EXPECT_EQ(display_name, firebase_user_->display_name()); + EXPECT_EQ(photo_url, firebase_user_->photo_url()); + + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + User::UserProfile blank_profile; + blank_profile.display_name = blank_profile.photo_url = ""; + WaitForFuture(firebase_user_->UpdateUserProfile(blank_profile)); + EXPECT_TRUE(firebase_user_->display_name().empty()); + EXPECT_TRUE(firebase_user_->photo_url().empty()); +} + +// Tests the happy case of unlinking a provider from the currently logged in +// user. +TEST_F(UserDesktopTest, TestUnlink) { + FakeSetT fakes; + // So that the user has an associated provider + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = FakeGetAccountInfoResponse(); + fakes[GetUrlForApi(API_KEY, "setAccountInfo")] = FakeSetAccountInfoResponse(); + InitializeConfigWithFakes(fakes); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->Reload()); + WaitForFuture(firebase_user_->Unlink("fake_provider_id")); + VerifyProviderData(*firebase_user_); +} + +TEST_F(UserDesktopTest, TestUnlink_NonLinkedProvider) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->Unlink("no_such_provider"), + kAuthErrorNoSuchProvider); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_OauthCredential) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Response contains a new ID token, but user should have stayed the same. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + EXPECT_TRUE(firebase_user_->is_anonymous()); + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const User* const user = + WaitForFuture(firebase_user_->LinkWithCredential(credential)); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_EmailCredential) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // Response contains a new ID token, but user should have stayed the same. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const std::string new_email = "new_fake_email@example.com"; + + EXPECT_NE(new_email, firebase_user_->email()); + + EXPECT_TRUE(firebase_user_->is_anonymous()); + const Credential credential = + EmailAuthProvider::GetCredential(new_email.c_str(), "fake_password"); + WaitForFuture(firebase_user_->LinkWithCredential(credential)); + EXPECT_EQ(new_email, firebase_user_->email()); + EXPECT_FALSE(firebase_user_->is_anonymous()); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_NeedsConfirmation) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "verifyAssertion"), + FakeSuccessfulResponse("verifyAssertion", "\"needConfirmation\": true")); + + // If response contains needConfirmation, the whole operation should fail, and + // current user should be unaffected. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->LinkWithCredential(credential), + kAuthErrorAccountExistsWithDifferentCredentials); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_ChecksAlreadyLinkedProviders) { + { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = + FakeVerifyAssertionResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = FakeSuccessfulResponse( + // clang-format off + "GetAccountInfoResponse", + std::string( + "\"users\":" + " [" + " {" + " \"localId\": \"localid123\",") + + GetFakeProviderInfo("google.com") + + " }" + " ]"); + // clang-format on + InitializeConfigWithFakes(fakes); + } + + // Upon linking, user should stay the same, but ID token should be updated. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential google_credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->LinkWithCredential(google_credential)); + + // The same provider shouldn't be linked twice. + WaitForFuture(firebase_user_->LinkWithCredential(google_credential), + kAuthErrorProviderAlreadyLinked); + + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + // Linking already linked provider, should fail, so current user shouldn't be + // updated at all. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = + FakeVerifyAssertionResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + // clang-format off + FakeSuccessfulResponse("GetAccountInfoResponse", + std::string( + "\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"providerUserInfo\": [") + + GetSingleFakeProvider("google.com") + "," + + GetSingleFakeProvider("facebook.com") + + " ]" + " }" + " ]"); + // clang-format on + InitializeConfigWithFakes(fakes); + } + + // Should be able to link a different provider. + const Credential facebook_credential = + FacebookAuthProvider::GetCredential("fake_access_token"); + WaitForFuture(firebase_user_->LinkWithCredential(facebook_credential)); + + // The same provider shouldn't be linked twice. + WaitForFuture(firebase_user_->LinkWithCredential(facebook_credential), + kAuthErrorProviderAlreadyLinked); + // Check that the previously linked provider wasn't overridden. + WaitForFuture(firebase_user_->LinkWithCredential(google_credential), + kAuthErrorProviderAlreadyLinked); +} + +TEST_F(UserDesktopTest, TestLinkWithCredentialAndRetrieveData) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Upon linking, user should stay the same, but ID token should be updated. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const SignInResult sign_in_result = WaitForFuture( + firebase_user_->LinkAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, TestReauthenticate) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Upon reauthentication, user should have stayed the same, but ID token + // should have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->Reauthenticate(credential)); +} + +TEST_F(UserDesktopTest, TestReauthenticate_NeedsConfirmation) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "verifyAssertion"), + FakeSuccessfulResponse("verifyAssertion", "\"needConfirmation\": true")); + + // If response contains needConfirmation, the whole operation should fail, and + // current user should be unaffected. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->Reauthenticate(credential), + kAuthErrorAccountExistsWithDifferentCredentials); +} + +TEST_F(UserDesktopTest, TestReauthenticateAndRetrieveData) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Upon reauthentication, user should have stayed the same, but ID token + // should have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const SignInResult sign_in_result = + WaitForFuture(firebase_user_->ReauthenticateAndRetrieveData(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); +} + +// Checks that current user is signed out upon receiving errors from the +// backend indicating the user is no longer valid. +class UserDesktopTestSignOutOnError : public UserDesktopTest { + protected: + // Reduces boilerplate in similar tests checking for sign out in several API + // methods. + template + void CheckSignOutIfUserIsInvalid(const std::string& api_endpoint, + const std::string& backend_error, + const AuthError sdk_error, + const OperationT operation) { + // Receiving error from the backend should make the operation fail, and + // current user shouldn't be affected. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + // First check that sign out doesn't happen on just any error + // (kAuthErrorOperationNotAllowed is chosen arbitrarily). + InitializeConfigWithAFake(api_endpoint, + CreateErrorHttpResponse("OPERATION_NOT_ALLOWED")); + EXPECT_FALSE(firebase_user_->uid().empty()); + WaitForFuture(operation(), kAuthErrorOperationNotAllowed); + EXPECT_FALSE(firebase_user_->uid().empty()); // User is still signed in. + + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + // Expect sign out. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Now check that the user will be logged out upon receiving a certain + // error from the backend. + InitializeConfigWithAFake(api_endpoint, + CreateErrorHttpResponse(backend_error)); + WaitForFuture(operation(), sdk_error); + EXPECT_THAT(firebase_user_->uid(), IsEmpty()); + } +}; + +TEST_F(UserDesktopTestSignOutOnError, Reauth) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "verifyAssertion"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->Reauthenticate( + GoogleAuthProvider::GetCredential("fake_id_token", "")); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, Reload) { + CheckSignOutIfUserIsInvalid(GetUrlForApi(API_KEY, "getAccountInfo"), + "USER_NOT_FOUND", kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->Reload(); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, UpdateEmail) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->UpdateEmail("fake_email@example.com"); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, UpdatePassword) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_DISABLED", + kAuthErrorUserDisabled, [&] { + sem_.Post(); + return firebase_user_->UpdatePassword("fake_password"); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, UpdateProfile) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "TOKEN_EXPIRED", + kAuthErrorUserTokenExpired, [&] { + sem_.Post(); + return firebase_user_->UpdateUserProfile(User::UserProfile()); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, Unlink) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "getAccountInfo"), + FakeGetAccountInfoResponse()); + WaitForFuture(firebase_user_->Reload()); + + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->Unlink("fake_provider_id"); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, LinkWithEmail) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->LinkWithCredential( + EmailAuthProvider::GetCredential("fake_email@example.com", + "fake_password")); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, LinkWithOauthCredential) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "verifyAssertion"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->LinkWithCredential( + GoogleAuthProvider::GetCredential("fake_id_token", "")); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, GetToken) { + const auto api_url = + std::string("https://securetoken.googleapis.com/v1/token?key=") + API_KEY; + CheckSignOutIfUserIsInvalid(api_url, "USER_NOT_FOUND", kAuthErrorUserNotFound, + [&] { + sem_.Post(); + return firebase_user_->GetToken(true); + }); + sem_.Wait(); +} + +// This test is to expose potential race condition and is primarily intended to +// be run with --config=tsan +TEST_F(UserDesktopTest, TestRaceCondition_SetAccountInfoAndSignOut) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // SignOut is engaged on the main thread, whereas UpdateEmail will be executed + // on the background thread; consequently, the order in which they are + // executed is not defined. Nevertheless, this should not lead to any data + // corruption, when UpdateEmail writes to user profile while it's being + // deleted by SignOut. Whichever method succeeds first, user must be signed + // out once both are finished: if SignOut finishes last, it overrides the + // updated user, and if UpdateEmail finishes last, it should note that there + // is no currently signed in user and fail with kAuthErrorUserNotFound. + + auto future = firebase_user_->UpdateEmail("some_email"); + firebase_auth_->SignOut(); + while (future.status() == firebase::kFutureStatusPending) { + } + + EXPECT_THAT(future.error(), AnyOf(kAuthErrorNone, kAuthErrorNoSignedInUser)); + EXPECT_EQ(nullptr, firebase_auth_->current_user()); +} + +// LinkWithProvider tests. +TEST_F(UserDesktopTest, TestLinkWithProviderReturnsUnsupportedError) { + FederatedOAuthProvider provider; + Future future = firebase_user_->LinkWithProvider(&provider); + EXPECT_EQ(future.result()->user, nullptr); + EXPECT_EQ(future.error(), kAuthErrorUnimplemented); + EXPECT_EQ(std::string(future.error_message()), + "Operation is not supported on non-mobile systems."); +} + +// TODO(drsanta) The following tests are disabled as the AuthHandler support has +// not yet been released. +TEST_F(UserDesktopTest, + DISABLED_TestLinkWithProviderAndHandlerPassingIntegrityChecks) { + FederatedOAuthProvider provider; + test::OAuthProviderTestHandler handler(/*extra_integrity_checks_=*/true); + InitializeSuccessfulAuthenticateWithProviderFlow(&provider, &handler); + + Future future = firebase_user_->LinkWithProvider(&provider); + handler.TriggerLinkComplete(); + SignInResult sign_in_result = WaitForFuture(future); +} + +TEST_F(UserDesktopTest, + DISABLED_TestPendingLinkWithProviderSecondConcurrentSignInFails) { + FederatedOAuthProvider provider1; + OAuthProviderTestHandler handler1; + InitializeSuccessfulAuthenticateWithProviderFlow(&provider1, &handler1); + + FederatedOAuthProvider provider2; + provider2.SetProviderData(GetFakeOAuthProviderData()); + + OAuthProviderTestHandler handler2; + provider2.SetAuthHandler(&handler2); + Future future1 = firebase_user_->LinkWithProvider(&provider1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = firebase_user_->LinkWithProvider(&provider2); + VerifySignInResult(future2, kAuthErrorFederatedProviderAreadyInUse); + handler1.TriggerLinkComplete(); + const SignInResult sign_in_result = WaitForFuture(future1); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkWithProviderSignInResultUserPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + FederatedAuthProvider::AuthenticatedUserData user_data = + *(handler.GetAuthenticatedUserData()); + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + EXPECT_NE(sign_in_result.user, nullptr); + EXPECT_EQ(sign_in_result.user->is_email_verified(), + user_data.is_email_verified); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + EXPECT_EQ(sign_in_result.user->uid(), user_data.uid); + EXPECT_EQ(sign_in_result.user->email(), user_data.email); + EXPECT_EQ(sign_in_result.user->display_name(), user_data.display_name); + EXPECT_EQ(sign_in_result.user->photo_url(), user_data.photo_url); + EXPECT_EQ(sign_in_result.user->provider_id(), user_data.provider_id); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullUIDFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->uid = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullDisplayNamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->display_name = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullUsernamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->user_name = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullPhotoUrlPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->photo_url = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullProvderIdFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->provider_id = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestLinkCompleteNuDISABLED_llAccessTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->access_token = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullRefreshTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->refresh_token = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteExpiresInMaxUInt64Passes) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->token_expires_in_seconds = ULONG_MAX; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteErrorMessagePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/false); + const char* error_message = "oh nos!"; + handler.TriggerLinkCompleteWithError(kAuthErrorApiNotAvailable, + error_message); + VerifySignInResult(future, kAuthErrorApiNotAvailable, error_message); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullErrorMessageFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/false); + handler.TriggerLinkCompleteWithError(kAuthErrorApiNotAvailable, nullptr); + VerifySignInResult(future, kAuthErrorApiNotAvailable); +} + +// ReauthenticateWithProvider tests. +TEST_F(UserDesktopTest, TestReauthentciateWithProviderReturnsUnsupportedError) { + FederatedOAuthProvider provider; + Future future = + firebase_user_->ReauthenticateWithProvider(&provider); + EXPECT_EQ(future.result()->user, nullptr); + EXPECT_EQ(future.error(), kAuthErrorUnimplemented); + EXPECT_EQ(std::string(future.error_message()), + "Operation is not supported on non-mobile systems."); +} + +// TODO(drsanta) The following tests are disabled as the AuthHandler support has +// not yet been released. +TEST_F( + UserDesktopTest, + DISABLED_TestReauthenticateWithProviderAndHandlerPassingIntegrityChecks) { + FederatedOAuthProvider provider; + test::OAuthProviderTestHandler handler(/*extra_integrity_checks_=*/true); + InitializeSuccessfulAuthenticateWithProviderFlow(&provider, &handler); + + Future future = + firebase_user_->ReauthenticateWithProvider(&provider); + handler.TriggerReauthenticateComplete(); + SignInResult sign_in_result = WaitForFuture(future); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateWithProviderSecondConcurrentSignInFails) { + FederatedOAuthProvider provider1; + OAuthProviderTestHandler handler1; + InitializeSuccessfulAuthenticateWithProviderFlow(&provider1, &handler1); + + FederatedOAuthProvider provider2; + provider2.SetProviderData(GetFakeOAuthProviderData()); + + OAuthProviderTestHandler handler2; + provider2.SetAuthHandler(&handler2); + Future future1 = + firebase_user_->ReauthenticateWithProvider(&provider1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = + firebase_user_->ReauthenticateWithProvider(&provider2); + VerifySignInResult(future2, kAuthErrorFederatedProviderAreadyInUse); + handler1.TriggerReauthenticateComplete(); + const SignInResult sign_in_result = WaitForFuture(future1); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateWithProviderSignInResultUserPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + FederatedAuthProvider::AuthenticatedUserData user_data = + *(handler.GetAuthenticatedUserData()); + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + EXPECT_NE(sign_in_result.user, nullptr); + EXPECT_EQ(sign_in_result.user->is_email_verified(), + user_data.is_email_verified); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + EXPECT_EQ(sign_in_result.user->uid(), user_data.uid); + EXPECT_EQ(sign_in_result.user->email(), user_data.email); + EXPECT_EQ(sign_in_result.user->display_name(), user_data.display_name); + EXPECT_EQ(sign_in_result.user->photo_url(), user_data.photo_url); + EXPECT_EQ(sign_in_result.user->provider_id(), user_data.provider_id); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullUIDFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->uid = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullDisplayNamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->display_name = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullUsernamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->user_name = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullPhotoUrlPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->photo_url = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullProvderIdFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->provider_id = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullAccessTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->access_token = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullRefreshTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->refresh_token = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteExpiresInMaxUInt64Passes) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->token_expires_in_seconds = ULONG_MAX; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteErrorMessagePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/false); + const char* error_message = "oh nos!"; + handler.TriggerReauthenticateCompleteWithError(kAuthErrorApiNotAvailable, + error_message); + VerifySignInResult(future, kAuthErrorApiNotAvailable, error_message); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullErrorMessageFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/false); + handler.TriggerReauthenticateCompleteWithError(kAuthErrorApiNotAvailable, + nullptr); + VerifySignInResult(future, kAuthErrorApiNotAvailable); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/user_test.cc b/auth/tests/user_test.cc new file mode 100644 index 0000000000..e38804c7cf --- /dev/null +++ b/auth/tests/user_test.cc @@ -0,0 +1,520 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/user.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/ticker.h" + +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) +#include "app/rest/transport_builder.h" +#include "app/rest/transport_mock.h" +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +namespace firebase { +namespace auth { + +namespace { + +// Wait for the Future completed when necessary. We do not do so for Android nor +// iOS since their test is based on Ticker-based fake. We do not do so for +// desktop stub since its Future completes immediately. +template +inline void MaybeWaitForFuture(const Future& future) { +// Desktop developer sdk has a small delay due to async calls. +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + // Once REST implementation is in, we should be able to check this. Almost + // always the return of last-result is ahead of the future completion. But + // right now, the return of last-result actually happens after future is + // completed. + // EXPECT_EQ(firebase::kFutureStatusPending, future.status()); + while (firebase::kFutureStatusPending == future.status()) { + } +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) +} + +const char* const SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE = + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"email\": \"new@email.com\"" + " }']" + " }" + " }"; + +const char* const VERIFY_PASSWORD_SUCCESSFUL_RESPONSE = + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"idToken\": \"idtoken123\"," + " \"registered\": true," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"" + " }']" + " }" + " }"; + +const char* const GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE = + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " users: [{" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\"," + " \"providerUserInfo\": [" + " {" + " \"providerId\": \"provider\"," + " }" + " ]" + " }]" + " }']" + " }" + " }"; + +} // anonymous namespace + +class UserTest : public ::testing::Test { + protected: + void SetUp() override { +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{ticker:0}}," + " {fake:'FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{ticker:0}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#SignupNewUserResponse\"," + " \"idToken\": \"idtoken123\"," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"," + " \"localId\": \"localid123\"" + "}',]" + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"users\": [{" + " \"localId\": \"localid123\"" + " }]}'," + " ]" + " }" + " }" + " ]" + "}"); + firebase_app_ = testing::CreateApp(); + firebase_auth_ = Auth::GetAuth(firebase_app_); + Future result = firebase_auth_->SignInAnonymously(); + MaybeWaitForFuture(result); + firebase_user_ = firebase_auth_->current_user(); + EXPECT_NE(nullptr, firebase_user_); + } + + void TearDown() override { + // We do not own firebase_user_ object. So just assign it to nullptr here. + firebase_user_ = nullptr; + delete firebase_auth_; + firebase_auth_ = nullptr; + delete firebase_app_; + firebase_app_ = nullptr; + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + // A helper function to verify future result naively: (1) it completed after + // one ticker and (2) the result has no error. Since most of the function in + // user delegate the actual logic into the native SDK, this verification is + // enough for most of the test case unless we implement some logic into the + // fake, which is not necessary for unit test. + template + static void Verify(const Future result) { +// Fake Android & iOS implemented the delay. Desktop stub completed immediately. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + MaybeWaitForFuture(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(0, result.error()); + } + + App* firebase_app_ = nullptr; + Auth* firebase_auth_ = nullptr; + User* firebase_user_ = nullptr; +}; + +TEST_F(UserTest, TestGetToken) { + // Test get sign-in token. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.getIdToken', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.getIDTokenForcingRefresh:completion:'," + " futuregeneric:{ticker:1}}," + " {" + " fake: '" + "https://securetoken.googleapis.com/v1/token?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"access_token\": \"fake_access_token\"," + " \"expires_in\": \"3600\"," + " \"token_type\": \"Bearer\"," + " \"refresh_token\": \"fake_refresh_token\"," + " \"id_token\": \"fake_id_token\"," + " \"user_id\": \"fake_user_id\"," + " \"project_id\": \"fake_project_id\"" + " }']" + " }" + " }" + " ]" + "}"); + Future token = + firebase_user_->GetToken(false /* force_refresh, doesn't matter here */); + + Verify(token); + EXPECT_FALSE(token.result()->empty()); +} + +TEST_F(UserTest, TestGetProviderData) { + // Test get provider data. Right now, most of the sign-in does not have extra + // data coming from providers. + const std::vector& provider = + firebase_user_->provider_data(); + EXPECT_TRUE(provider.empty()); +} + +TEST_F(UserTest, TestUpdateEmail) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.updateEmail', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.updateEmail:completion:', futuregeneric:" + "{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + EXPECT_NE("new@email.com", firebase_user_->email()); + Future result = firebase_user_->UpdateEmail("new@email.com"); + +// Fake Android & iOS implemented the delay. Desktop stub completed immediately. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + EXPECT_NE("new@email.com", firebase_user_->email()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + MaybeWaitForFuture(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(0, result.error()); + EXPECT_EQ("new@email.com", firebase_user_->email()); +} + +TEST_F(UserTest, TestUpdatePassword) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.updatePassword', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.updatePassword:completion:'," + " futuregeneric:{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->UpdatePassword("1234567"); + Verify(result); +} + +TEST_F(UserTest, TestUpdateUserProfile) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.updateProfile', futuregeneric:{ticker:1}}," + " {fake:'FIRUserProfileChangeRequest." + "commitChangesWithCompletion:'," + " futuregeneric:{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + User::UserProfile profile; + Future result = firebase_user_->UpdateUserProfile(profile); + Verify(result); +} + +TEST_F(UserTest, TestReauthenticate) { + // Test reauthenticate. + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.reauthenticate', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.reauthenticateWithCredential:completion:'," + " futuregeneric:{ticker:1}},") + + VERIFY_PASSWORD_SUCCESSFUL_RESPONSE + "," + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->Reauthenticate( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} + +#if !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) +TEST_F(UserTest, TestReauthenticateAndRetrieveData) { + // Test reauthenticate and retrieve data. + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.reauthenticateAndRetrieveData'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRUser.reauthenticateAndRetrieveDataWithCredential:" + "completion:'," + " futuregeneric:{ticker:1}},") + + VERIFY_PASSWORD_SUCCESSFUL_RESPONSE + "," + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->ReauthenticateAndRetrieveData( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} +#endif // !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +TEST_F(UserTest, TestSendEmailVerification) { + // Test send email verification. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.sendEmailVerification'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRUser.sendEmailVerificationWithCompletion:'," + " futuregeneric:{ticker:1}}," + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#GetOobConfirmationCodeResponse\"," + " \"email\": \"fake_email@fake_domain.com\"" + " }']" + " }" + " }" + " ]" + "}"); + Future result = firebase_user_->SendEmailVerification(); + Verify(result); +} + +TEST_F(UserTest, TestLinkWithCredential) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.linkWithCredential', " + "futuregeneric:{ticker:1}}," + " {fake:'FIRUser.linkWithCredential:completion:'," + " futuregeneric:{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->LinkWithCredential( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} + +#if !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) +TEST_F(UserTest, TestLinkAndRetrieveDataWithCredential) { + // Test link and retrieve data with credential. This calls the same native SDK + // function as LinkWithCredential(). + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.linkWithCredential', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.linkAndRetrieveDataWithCredential:completion:'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Future result = + firebase_user_->LinkAndRetrieveDataWithCredential( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} +#endif // !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +TEST_F(UserTest, TestUnlink) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.unlink', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.unlinkFromProvider:completion:'," + " futuregeneric:{ticker:1}},") + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + "," + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + // Mobile wrappers and desktop have different implementations: desktop checks + // for valid provider before doing the RPC call, while wrappers leave that to + // platform implementation, which is faked out in the test. To minimize the + // divergence, for desktop only, first prepare server GetAccountInfo response + // which contains a provider, and then Reload, to make sure that the given + // provider ID is valid. For mobile wrappers, this will be a no-op. Use + // MaybeWaitForFuture because to Reload will return immediately for mobile + // wrappers, and Verify expects at least a single "tick". + MaybeWaitForFuture(firebase_user_->Reload()); + Future result = firebase_user_->Unlink("provider"); + Verify(result); + // For desktop, the provider must have been removed. For mobile wrappers, the + // whole flow must have been a no-op, and the provider list was empty to begin + // with. + EXPECT_TRUE(firebase_user_->provider_data().empty()); +} + +TEST_F(UserTest, TestReload) { + // Test reload. + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.reload', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.reloadWithCompletion:', " + "futuregeneric:{ticker:1}},") + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->Reload(); + Verify(result); +} + +TEST_F(UserTest, TestDelete) { + // Test delete. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.delete', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.deleteWithCompletion:', futuregeneric:{ticker:1}}," + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "deleteAccount?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#DeleteAccountResponse\"" + " }']" + " }" + " }" + " ]" + "}"); + Future result = firebase_user_->Delete(); + Verify(result); +} + +TEST_F(UserTest, TestIsEmailVerified) { + // Test is email verified. Right now both stub and fake will return false + // unanimously. + EXPECT_FALSE(firebase_user_->is_email_verified()); +} + +TEST_F(UserTest, TestIsAnonymous) { + // Test is anonymous. + EXPECT_TRUE(firebase_user_->is_anonymous()); +} + +TEST_F(UserTest, TestGetter) { +// Test getter functions. The fake value are different between stub and fake. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ("fake email", firebase_user_->email()); + EXPECT_EQ("fake display name", firebase_user_->display_name()); + EXPECT_EQ("fake provider id", firebase_user_->provider_id()); +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_TRUE(firebase_user_->email().empty()); + EXPECT_TRUE(firebase_user_->display_name().empty()); + EXPECT_EQ("Firebase", firebase_user_->provider_id()); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + + EXPECT_FALSE(firebase_user_->uid().empty()); + EXPECT_TRUE(firebase_user_->photo_url().empty()); +} +} // namespace auth +} // namespace firebase diff --git a/binary_to_array_test.py b/binary_to_array_test.py new file mode 100644 index 0000000000..3deee34cb7 --- /dev/null +++ b/binary_to_array_test.py @@ -0,0 +1,91 @@ +# Copyright 2018 Google LLC +# +# 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. + +"""Tests for google3.firebase.app.client.cpp.binary_to_array.""" + +from google3.testing.pybase import googletest +from google3.firebase.app.client.cpp import binary_to_array + +EXPECTED_SOURCE_FILE = """\ +// Copyright 2016 Google Inc. All Rights Reserved. + +#include + +namespace test_outer_namespace { +namespace test_inner_namespace { + +extern const size_t test_array_name_size; +extern const char test_fileid[]; +extern const unsigned char test_array_name[]; + +const unsigned char test_array_name[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x00 // Extra \\0 to make it a C string +}; + +const size_t test_array_name_size = + sizeof(test_array_name) - 1; + +const char test_fileid[] = + "test_filename"; + +} // namespace test_inner_namespace +} // namespace test_outer_namespace +""" + +EXPECTED_HEADER_FILE = """\ +// Copyright 2016 Google Inc. All Rights Reserved. + +#ifndef TEST_HEADER_GUARD +#define TEST_HEADER_GUARD + +#include + +namespace test_outer_namespace { +namespace test_inner_namespace { + +extern const size_t test_array_name_size; +extern const unsigned char test_array_name[]; +extern const char test_fileid[]; + +} // namespace test_inner_namespace +} // namespace test_outer_namespace + +#endif // TEST_HEADER_GUARD +""" + +namespaces = ["test_outer_namespace", "test_inner_namespace"] +array_name = "test_array_name" +array_size_name = "test_array_name_size" +fileid = "test_fileid" +filename = "test_filename" +input_bytes = [1, 2, 3, 4, 5, 6, 7] +header_guard = "TEST_HEADER_GUARD" + + +class BinaryToArrayTest(googletest.TestCase): + + def test_source_file(self): + result_source = binary_to_array.source( + namespaces, array_name, array_size_name, fileid, filename, input_bytes) + self.assertEqual("\n".join(result_source), EXPECTED_SOURCE_FILE) + + def test_header_file(self): + result_header = binary_to_array.header(header_guard, namespaces, array_name, + array_size_name, fileid) + self.assertEqual("\n".join(result_header), EXPECTED_HEADER_FILE) + + +if __name__ == "__main__": + googletest.main() diff --git a/build_type_header_test.py b/build_type_header_test.py new file mode 100644 index 0000000000..45e8e775ef --- /dev/null +++ b/build_type_header_test.py @@ -0,0 +1,46 @@ +# Copyright 2018 Google LLC +# +# 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. + +"""Tests for google3.firebase.app.client.cpp.build_type_header.""" + +from google3.testing.pybase import googletest +from google3.firebase.app.client.cpp import build_type_header + +EXPECTED_BUILD_TYPE_HEADER = """\ +// Copyright 2017 Google Inc. All Rights Reserved. + +#ifndef FIREBASE_APP_CLIENT_CPP_SRC_BUILD_TYPE_H_ +#define FIREBASE_APP_CLIENT_CPP_SRC_BUILD_TYPE_H_ + +// Available build configurations for the suite of libraries. +#define FIREBASE_CPP_BUILD_TYPE_HEAD 0 +#define FIREBASE_CPP_BUILD_TYPE_STABLE 1 +#define FIREBASE_CPP_BUILD_TYPE_RELEASED 2 + +// Currently selected build type. +#define FIREBASE_CPP_BUILD_TYPE TEST_BUILD_TYPE + +#endif // FIREBASE_APP_CLIENT_CPP_SRC_BUILD_TYPE_H_ +""" + + +class BuildTypeHeaderTest(googletest.TestCase): + + def test_build_type_header(self): + result_header = build_type_header.generate_header('TEST_BUILD_TYPE') + self.assertEqual(result_header, EXPECTED_BUILD_TYPE_HEADER) + + +if __name__ == '__main__': + googletest.main() diff --git a/database/src/ios/util_ios_test.mm b/database/src/ios/util_ios_test.mm new file mode 100644 index 0000000000..ef0286f240 --- /dev/null +++ b/database/src/ios/util_ios_test.mm @@ -0,0 +1,111 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#import + +#include "app/src/util_ios.h" + +using ::firebase::Variant; +using ::firebase::util::VariantToId; + +@interface VariantToIdTests : XCTestCase +@end + +@implementation VariantToIdTests + +- (void)testNull { + XCTAssertEqual(VariantToId(Variant::Null()), [NSNull null]); +} + +- (void)testInt64WithZero { + id value_id = VariantToId(Variant::FromInt64(0LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 0LL); +} + +- (void)testInt64WithSigned32BitValue { + id value_id = VariantToId(Variant::FromInt64(2000000000LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 2000000000LL); +} + +- (void)testInt64WithLongLongValue { + id value_id = VariantToId(Variant::FromInt64(8000000000LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 8000000000LL); +} + +- (void)testInt64WithLargeValue { + id value_id = VariantToId(Variant::FromInt64(636900045569749380LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 636900045569749380LL); +} + +- (void)testDoubleWithZeroPointZero { + id value_id = VariantToId(Variant::ZeroPointZero()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.doubleValue, 0.0); +} + +- (void)testDoubleWithOnePointZero { + id value_id = VariantToId(Variant::OnePointZero()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.doubleValue, 1.0); +} + +- (void)testDoubleWithPi { + id value_id = VariantToId(Variant::FromDouble(3.14159)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.doubleValue, 3.14159); +} + +- (void)testBoolWithTrue { + id value_id = VariantToId(Variant::True()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.boolValue, true); +} + +- (void)testBoolWithFalse { + id value_id = VariantToId(Variant::False()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.boolValue, false); +} + +- (void)testStaticStringWithEmptyString { + id value_id = VariantToId(Variant::FromStaticString("")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @""); +} + +- (void)testStaticStringWithNonEmptyString { + id value_id = VariantToId(Variant::FromStaticString("Hello, world!")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @"Hello, world!"); +} + +- (void)testMutableStringWithEmptyString { + id value_id = VariantToId(Variant::FromMutableString("")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @""); +} + +- (void)testMutableStringWithNonEmptyString { + id value_id = VariantToId(Variant::FromMutableString("Hello, world!")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @"Hello, world!"); +} + +@end diff --git a/database/tests/CMakeLists.txt b/database/tests/CMakeLists.txt new file mode 100644 index 0000000000..5a8e416c23 --- /dev/null +++ b/database/tests/CMakeLists.txt @@ -0,0 +1,343 @@ +# Copyright 2019 Google LLC +# +# 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. + +firebase_cpp_cc_test( + firebase_rtdb_util_desktop_test + SOURCES + desktop/util_desktop_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_indexed_variant_test + SOURCES + desktop/core/indexed_variant_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_tracked_query_manager_test + SOURCES + desktop/core/tracked_query_manager_test.cc + desktop/test/mock_persistence_storage_engine.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_compound_write_test + SOURCES + desktop/core/compound_write_test.cc + DEPENDS + firebase_database + firebase_testing + +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_tree_test + SOURCES + desktop/core/tree_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_child_change_accumulator_test + SOURCES + desktop/view/child_change_accumulator_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_cache_test + SOURCES + desktop/view/view_cache_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_operation_test + SOURCES + desktop/core/operation_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_write_tree_test + SOURCES + desktop/core/write_tree_test.cc + desktop/test/mock_write_tree.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_indexed_filter_test + SOURCES + desktop/view/indexed_filter_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_limited_filter_test + SOURCES + desktop/view/limited_filter_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_ranged_filter_test + SOURCES + desktop/view/ranged_filter_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_test + SOURCES + desktop/test/matchers.h + desktop/view/view_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_matchers_test + SOURCES + desktop/test/matchers.h + desktop/test/matchers_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_processor_test + SOURCES + desktop/view/view_processor_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_persistence_manager_test + SOURCES + desktop/persistence/persistence_manager_test.cc + desktop/test/mock_cache_policy.h + desktop/test/mock_persistence_storage_engine.h + desktop/test/mock_tracked_query_manager.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_noop_persistence_manager_test + SOURCES + desktop/persistence/noop_persistence_manager_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_cache_policy_test + SOURCES + desktop/core/cache_policy_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_prune_forest_test + SOURCES + desktop/persistence/prune_forest_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_in_memory_persistence_storage_engine_test + SOURCES + desktop/persistence/in_memory_persistence_storage_engine_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_flatbuffer_conversion_test + SOURCES + desktop/persistence/flatbuffer_conversions_test.cc + DEPENDS + firebase_database + firebase_testing + flexbuffer_matcher +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_sync_point_test + SOURCES + desktop/core/sync_point_test.cc + desktop/test/matchers.h + desktop/test/mock_cache_policy.h + desktop/test/mock_listener.h + desktop/test/mock_persistence_manager.h + desktop/test/mock_persistence_storage_engine.h + desktop/test/mock_tracked_query_manager.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_sync_tree_test + SOURCES + desktop/core/sync_tree_test.cc + desktop/test/mock_listen_provider.h + desktop/test/mock_listener.h + desktop/test/mock_persistence_manager.h + desktop/test/mock_persistence_storage_engine.h + desktop/test/mock_tracked_query_manager.h + desktop/test/mock_write_tree.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_server_values_test + SOURCES + desktop/core/server_values_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_sparse_snapshot_tree_test + SOURCES + desktop/core/sparse_snapshot_tree_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_event_registration_test + SOURCES + desktop/core/event_registration_test.cc + desktop/test/mock_listener.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_mutable_data_desktop_test + SOURCES + desktop/mutable_data_desktop_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_change_test + SOURCES + desktop/view/change_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_event_generator_test + SOURCES + desktop/view/event_generator_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_common_database_reference_test + SOURCES + common/database_reference_test.cc + DEPENDS + firebase_app_for_testing + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_connection_web_socket_client_impl_test + SOURCES + desktop/connection/web_socket_client_impl_test.cc + INCLUDES + ${OPENSSL_INCLUDE_DIR} + ${UWEBSOCKETS_SOURCE_DIR}/.. + DEPENDS + firebase_database + firebase_testing + ${OPENSSL_CRYPTO_LIBRARY} + libuWS +) + +if(MSVC) + target_compile_definitions(firebase_rtdb_desktop_connection_web_socket_client_impl_test + PRIVATE + -DWIN32_LEAN_AND_MEAN + ) +endif() + +firebase_cpp_cc_test( + firebase_rtdb_desktop_connection_connection_test + SOURCES + desktop/connection/connection_test.cc + INCLUDES + ${OPENSSL_INCLUDE_DIR} + ${UWEBSOCKETS_SOURCE_DIR}/.. + DEPENDS + ${OPENSSL_CRYPTO_DIR} + libuWS + firebase_app_for_testing + firebase_database + firebase_testing +) + diff --git a/database/tests/common/database_reference_test.cc b/database/tests/common/database_reference_test.cc new file mode 100644 index 0000000000..8d83547181 --- /dev/null +++ b/database/tests/common/database_reference_test.cc @@ -0,0 +1,283 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/include/firebase/database/database_reference.h" + +#include + +#include "app/src/include/firebase/app.h" +#include "app/src/thread.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/database_reference.h" +#include "database/src/include/firebase/database.h" + +using firebase::App; +using firebase::AppOptions; + +using testing::Eq; + +static const char kApiKey[] = "MyFakeApiKey"; +static const char kDatabaseUrl[] = "https://abc-xyz-123.firebaseio.com"; + +namespace firebase { +namespace database { + +class DatabaseReferenceTest : public ::testing::Test { + public: + void SetUp() override { + AppOptions options = testing::MockAppOptions(); + options.set_database_url(kDatabaseUrl); + options.set_api_key(kApiKey); + app_ = testing::CreateApp(options); + database_ = Database::GetInstance(app_); + } + + void DeleteDatabase() { + delete database_; + database_ = nullptr; + } + + void TearDown() override { + delete database_; + delete app_; + } + + protected: + App* app_; + Database* database_; +}; + +// Test DatabaseReference() +TEST_F(DatabaseReferenceTest, DefaultConstructor) { + DatabaseReference ref; + EXPECT_FALSE(ref.is_valid()); +} + +// Test DatabaseReference(DatabaseReferenceInternal*) +TEST_F(DatabaseReferenceTest, ConstructorWithInternalPointer) { + // Assume Database::GetReference() would utilize DatabaseReferenceInternal* + // created from different platform-dependent code to create + // DatabaseReference. + EXPECT_TRUE(database_->GetReference().is_valid()); + EXPECT_TRUE(database_->GetReference().is_root()); + EXPECT_THAT(database_->GetReference().key_string(), Eq("")); + + EXPECT_TRUE(database_->GetReference("child").is_valid()); + EXPECT_FALSE(database_->GetReference("child").is_root()); + EXPECT_THAT(database_->GetReference("child").key_string(), Eq("child")); +} + +// Test DatabaseReference(const DatabaseReference&) +TEST_F(DatabaseReferenceTest, CopyConstructor) { + DatabaseReference ref_null; + DatabaseReference ref_copy_null(ref_null); + EXPECT_FALSE(ref_copy_null.is_valid()); + + DatabaseReference ref_copy_root(database_->GetReference()); + EXPECT_TRUE(ref_copy_root.is_valid()); + EXPECT_TRUE(ref_copy_root.is_root()); + EXPECT_THAT(ref_copy_root.key_string(), Eq("")); + + DatabaseReference ref_copy_child(database_->GetReference("child")); + EXPECT_TRUE(ref_copy_child.is_valid()); + EXPECT_FALSE(ref_copy_child.is_root()); + EXPECT_THAT(ref_copy_child.key_string(), Eq("child")); +} + +// Test DatabaseReference(DatabaseReference&&) +TEST_F(DatabaseReferenceTest, MoveConstructor) { + DatabaseReference ref_null; + DatabaseReference ref_move_null(std::move(ref_null)); + EXPECT_FALSE(ref_move_null.is_valid()); + + DatabaseReference ref_root = database_->GetReference(); + DatabaseReference ref_move_root(std::move(ref_root)); + EXPECT_FALSE(ref_root.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_root.is_valid()); + EXPECT_TRUE(ref_move_root.is_root()); + EXPECT_THAT(ref_move_root.key_string(), Eq("")); + + DatabaseReference ref_child = database_->GetReference("child"); + DatabaseReference ref_move_child(std::move(ref_child)); + EXPECT_FALSE(ref_child.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_child.is_valid()); + EXPECT_FALSE(ref_move_child.is_root()); + EXPECT_THAT(ref_move_child.key_string(), Eq("child")); +} + +// Test operator=(const DatabaseReference&) +TEST_F(DatabaseReferenceTest, CopyOperator) { + DatabaseReference ref_copy_null; + ref_copy_null = DatabaseReference(); + EXPECT_FALSE(ref_copy_null.is_valid()); + + DatabaseReference ref_copy_root; + ref_copy_root = database_->GetReference(); + EXPECT_TRUE(ref_copy_root.is_valid()); + EXPECT_TRUE(ref_copy_root.is_root()); + EXPECT_THAT(ref_copy_root.key_string(), Eq("")); + + DatabaseReference ref_copy_child; + ref_copy_child = database_->GetReference("child"); + EXPECT_TRUE(ref_copy_child.is_valid()); + EXPECT_FALSE(ref_copy_child.is_root()); + EXPECT_THAT(ref_copy_child.key_string(), Eq("child")); +} + +// Test operator=(DatabaseReference&&) +TEST_F(DatabaseReferenceTest, MoveOperator) { + DatabaseReference ref_null; + DatabaseReference ref_move_null; + ref_move_null = std::move(ref_null); + EXPECT_FALSE(ref_move_null.is_valid()); + + DatabaseReference ref_root = database_->GetReference(); + DatabaseReference ref_move_root; + ref_move_root = std::move(ref_root); + EXPECT_FALSE(ref_root.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_root.is_valid()); + EXPECT_TRUE(ref_move_root.is_root()); + EXPECT_THAT(ref_move_root.key_string(), Eq("")); + + DatabaseReference ref_child = database_->GetReference("child"); + DatabaseReference ref_move_child; + ref_move_child = std::move(ref_child); + EXPECT_FALSE(ref_child.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_child.is_valid()); + EXPECT_FALSE(ref_move_child.is_root()); + EXPECT_THAT(ref_move_child.key_string(), Eq("child")); +} + +TEST_F(DatabaseReferenceTest, CleanupFunction) { + // Reused temporary DatabaseReference to be move to another DatabaseReference + DatabaseReference ref_to_be_moved; + + // Null DatabaseReference created through default constructor, copy + // constructor, copy operator, move constructor and move operator + DatabaseReference ref_null; + DatabaseReference ref_copy_const_null(ref_null); + DatabaseReference ref_copy_op_null; + ref_copy_op_null = ref_null; + ref_to_be_moved = ref_null; + DatabaseReference ref_move_const_null(std::move(ref_to_be_moved)); + ref_to_be_moved = ref_null; + DatabaseReference ref_move_op_null; + ref_move_op_null = std::move(ref_to_be_moved); + + // Root DatabaseReference created through default constructor, copy + // constructor, copy operator, move constructor and move operator + DatabaseReference ref_root = database_->GetReference(); + DatabaseReference ref_copy_const_root(ref_root); + DatabaseReference ref_copy_op_root; + ref_copy_op_root = ref_root; + ref_to_be_moved = ref_root; + DatabaseReference ref_move_const_root(std::move(ref_to_be_moved)); + ref_to_be_moved = ref_root; + DatabaseReference ref_move_op_root; + ref_move_op_root = std::move(ref_to_be_moved); + + // Child DatabaseReference created through default constructor, copy + // constructor, copy operator, move constructor and move operator + DatabaseReference ref_child = database_->GetReference("child"); + DatabaseReference ref_copy_const_child(ref_child); + DatabaseReference ref_copy_op_child; + ref_copy_op_child = ref_child; + ref_to_be_moved = ref_child; + DatabaseReference ref_move_const_child(std::move(ref_to_be_moved)); + ref_to_be_moved = ref_child; + DatabaseReference ref_move_op_child; + ref_move_op_child = std::move(ref_to_be_moved); + + DeleteDatabase(); + + EXPECT_FALSE(ref_null.is_valid()); + EXPECT_FALSE(ref_copy_const_null.is_valid()); + EXPECT_FALSE(ref_copy_op_null.is_valid()); + EXPECT_FALSE(ref_move_const_null.is_valid()); + EXPECT_FALSE(ref_move_op_null.is_valid()); + + EXPECT_FALSE(ref_root.is_valid()); + EXPECT_FALSE(ref_copy_const_root.is_valid()); + EXPECT_FALSE(ref_copy_op_root.is_valid()); + EXPECT_FALSE(ref_move_const_root.is_valid()); + EXPECT_FALSE(ref_move_op_root.is_valid()); + + EXPECT_FALSE(ref_child.is_valid()); + EXPECT_FALSE(ref_copy_const_child.is_valid()); + EXPECT_FALSE(ref_copy_op_child.is_valid()); + EXPECT_FALSE(ref_move_const_child.is_valid()); + EXPECT_FALSE(ref_move_op_child.is_valid()); + + EXPECT_FALSE(ref_to_be_moved.is_valid()); // NOLINT +} + +// Ensure that creating and moving around DatabaseReferences in one thread while +// the Database is deleted from another thread still properly cleans up all +// DatabaseReferences. +TEST_F(DatabaseReferenceTest, RaceConditionTest) { + struct TestUserdata { + DatabaseReference ref_null; + DatabaseReference ref_root; + DatabaseReference ref_child; + }; + + const int kThreadCount = 100; + std::vector threads; + threads.reserve(kThreadCount); + + for (int i = 0; i < kThreadCount; i++) { + TestUserdata* userdata = new TestUserdata; + userdata->ref_root = database_->GetReference(); + userdata->ref_child = database_->GetReference("child"); + + threads.emplace_back( + [](void* void_userdata) { + TestUserdata* userdata = static_cast(void_userdata); + + // If the Database has not been deletd, these DatabaseReferences are + // valid. If the Database has been deleted, these DatabaseReferences + // should be automatically emptied. + // + // We don't know if the Database has been deleted or not yet (and thus + // whether these DatabaseReferences are empty or not), so there's not + // really any test we can do on them other than to ensure that calling + // various constructors on them doesn't crash. + DatabaseReference ref_move_null; + ref_move_null = std::move(userdata->ref_null); + (void)ref_move_null; + + DatabaseReference ref_move_root; + ref_move_root = std::move(userdata->ref_root); + (void)ref_move_root; + + DatabaseReference ref_move_child; + ref_move_child = std::move(userdata->ref_child); + (void)ref_move_child; + + delete userdata; + }, + userdata); + } + + DeleteDatabase(); + + for (Thread& t : threads) { + t.Join(); + } +} + +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/connection/connection_test.cc b/database/tests/desktop/connection/connection_test.cc new file mode 100644 index 0000000000..57359c8295 --- /dev/null +++ b/database/tests/desktop/connection/connection_test.cc @@ -0,0 +1,301 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/connection/connection.h" + +#include + +#include "app/src/include/firebase/app.h" +#include "app/src/scheduler.h" +#include "app/src/semaphore.h" +#include "app/src/time.h" +#include "app/src/variant_util.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +static const char kDatabaseHostname[] = "cpp-database-test-app.firebaseio.com"; +static const char kDatabaseNamespace[] = "cpp-database-test-app"; + +namespace firebase { +namespace database { +namespace internal { +namespace connection { + +class ConnectionTest : public ::testing::Test, public ConnectionEventHandler { + protected: + ConnectionTest() + : test_host_info_(nullptr), + sem_on_cache_host_(0), + sem_on_ready_(0), + sem_on_data_message_(0), + sem_on_disconnect_(0) {} + + void SetUp() override { + testing::CreateApp(); + test_host_info_ = new HostInfo(kDatabaseHostname, kDatabaseNamespace, true); + } + + void TearDown() override { + delete firebase::App::GetInstance(); + delete test_host_info_; + } + + void OnCacheHost(const std::string& host) override { + LogDebug("OnCacheHost: %s", host.c_str()); + sem_on_cache_host_.Post(); + } + + void OnReady(int64_t timestamp, const std::string& sessionId) override { + LogDebug("OnReady: %lld, %s", timestamp, sessionId.c_str()); + last_session_id_ = sessionId; + sem_on_ready_.Post(); + } + + void OnDataMessage(const Variant& data) override { + LogDebug("OnDataMessage: %s", util::VariantToJson(data).c_str()); + sem_on_data_message_.Post(); + } + + void OnDisconnect(Connection::DisconnectReason reason) override { + LogDebug("OnDisconnect: %d", static_cast(reason)); + sem_on_disconnect_.Post(); + } + + void OnKill(const std::string& reason) override { + LogDebug("OnKill: %s", reason.c_str()); + } + + void ScheduledOpen(Connection* connection) { + scheduler_.Schedule(new callback::CallbackValue1( + connection, [](Connection* connection) { connection->Open(); })); + } + + void ScheduledSend(Connection* connection, const Variant& message) { + scheduler_.Schedule(new callback::CallbackValue2( + connection, message, [](Connection* connection, Variant message) { + connection->Send(message, false); + })); + } + + void ScheduledClose(Connection* connection) { + scheduler_.Schedule(new callback::CallbackValue1( + connection, [](Connection* connection) { connection->Close(); })); + } + + HostInfo GetHostInfo() { + assert(test_host_info_ != nullptr); + if (test_host_info_) { + return *test_host_info_; + } else { + return HostInfo(); + } + } + + scheduler::Scheduler scheduler_; + + HostInfo* test_host_info_; + + std::string last_session_id_; + + Semaphore sem_on_cache_host_; + Semaphore sem_on_ready_; + Semaphore sem_on_data_message_; + Semaphore sem_on_disconnect_; +}; + +static const int kTimeoutMs = 5000; + +TEST_F(ConnectionTest, DeleteConnectionImmediately) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); +} + +TEST_F(ConnectionTest, OpenConnection) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, CloseConnection) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + ScheduledClose(&connection); + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, MultipleConnections) { + const int kNumOfConnections = 10; + std::vector connections; + + Logger logger(nullptr); + for (int i = 0; i < kNumOfConnections; ++i) { + connections.push_back( + new Connection(&scheduler_, GetHostInfo(), nullptr, this, &logger)); + } + + for (auto& itConnection : connections) { + ScheduledOpen(itConnection); + } + + for (int i = 0; i < connections.size(); ++i) { + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + } + + for (auto& itConnection : connections) { + ScheduledClose(itConnection); + } + + for (int i = 0; i < connections.size(); ++i) { + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); + } + + for (int i = 0; i < kNumOfConnections; ++i) { + delete connections[i]; + connections[i] = nullptr; + } +} + +TEST_F(ConnectionTest, LastSession) { + Logger logger(nullptr); + Connection connection1(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection1); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + Connection connection2(&scheduler_, GetHostInfo(), last_session_id_.c_str(), + this, &logger); + + ScheduledOpen(&connection2); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + // connection1 disconnected + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); + + ScheduledClose(&connection2); + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); +} + +const char* const kWireProtocolClearRoot = + "{\"r\":1,\"a\":\"p\",\"b\":{\"p\":\"/connection/ConnectionTest/" + "\",\"d\": null}}"; +const char* const kWireProtocolListenRoot = + "{\"r\":2,\"a\":\"q\",\"b\":{\"p\":\"/connection/ConnectionTest/" + "\",\"h\":\"\"}}"; + +TEST_F(ConnectionTest, SimplePutRequest) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + ScheduledSend(&connection, util::JsonToVariant(kWireProtocolClearRoot)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, LargeMessage) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + ScheduledSend(&connection, util::JsonToVariant(kWireProtocolClearRoot)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); + + ScheduledSend(&connection, util::JsonToVariant(kWireProtocolListenRoot)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); + + // Send a long message + std::stringstream ss; + ss << "{\"r\":3,\"a\":\"p\",\"b\":{\"p\":\"/connection/ConnectionTest/" + "\",\"d\":\""; + for (int i = 0; i < 20000; ++i) { + ss << "!"; + } + ss << "\"}}"; + + ScheduledSend(&connection, util::JsonToVariant(ss.str().c_str())); + + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, TestBadHost) { + HostInfo bad_host("bad-host-name.bad", "bad-namespace", true); + Logger logger(nullptr); + Connection connection(&scheduler_, bad_host, nullptr, this, &logger); + connection.Open(); + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, TestCreateDestroyRace) { + Logger logger(nullptr); + // Test race when connecting to a valid host without sleep + // Try this on real server less time or the server may block this client + for (int i = 0; i < 10; ++i) { + Connection* connection = + new Connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + connection->Open(); + delete connection; + } + + // Test race when connecting to a valid host with sleep, to wait for websocket + // thread to kick-in + for (int i = 0; i < 10; ++i) { + Connection* connection = + new Connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + connection->Open(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + delete connection; + } + + // Test race when connecting to a bad host name without sleep + HostInfo bad_host("bad-host-name.bad", "bad-namespace", true); + for (int i = 0; i < 100; ++i) { + Connection* connection = + new Connection(&scheduler_, bad_host, nullptr, this, &logger); + connection->Open(); + delete connection; + } + + // Test race when connecting to a bad host name with sleep, to wait for + // websocket thread to kick-in + for (int i = 0; i < 100; ++i) { + Connection* connection = + new Connection(&scheduler_, bad_host, nullptr, this, &logger); + connection->Open(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + delete connection; + } +} + +} // namespace connection +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/connection/web_socket_client_impl_test.cc b/database/tests/desktop/connection/web_socket_client_impl_test.cc new file mode 100644 index 0000000000..a79fcc8ca4 --- /dev/null +++ b/database/tests/desktop/connection/web_socket_client_impl_test.cc @@ -0,0 +1,268 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/connection/web_socket_client_impl.h" +#include "app/src/semaphore.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace connection { + +// Simple WebSocket based Echo Server using third_party/uWebSockets +// It has some quirk. Ex. hub_ needs a handler (async_) to wake the loop before +// closing it or the event loop will never stop. +class TestWebSocketEchoServer { + public: + explicit TestWebSocketEchoServer(int port) + : port_(port), run_(false), thread_(nullptr), keep_alive_(nullptr) { + hub_.onMessage([](uWS::WebSocket* ws, char* message, + size_t length, uWS::OpCode opCode) { + // Echo back immediately + ws->send(message, length, opCode); + }); + hub_.onConnection( + [](uWS::WebSocket* ws, uWS::HttpRequest request) { + LogDebug("[Server] Received connection from (%s) %s port: %d", + ws->getAddress().family, ws->getAddress().address, + ws->getAddress().port); + }); + hub_.onDisconnection([](uWS::WebSocket* ws, int code, + char* message, size_t length) { + LogDebug("[Server] Disconnected from (%s) %s port: %d", + ws->getAddress().family, ws->getAddress().address, + ws->getAddress().port); + }); + } + + ~TestWebSocketEchoServer() { Stop(); } + + void Start() { + keep_alive_ = new uS::Async(hub_.getLoop()); + keep_alive_->setData(this); + keep_alive_->start([](uS::Async* async) { + TestWebSocketEchoServer* server = + static_cast(async->getData()); + assert(server != nullptr); + // close ths group in event loop thread + server->hub_.getDefaultGroup().close(); + async->close(); + }); + + run_ = true; + thread_ = new std::thread([this]() { + auto listen = [&](int port){ + if (hub_.listen(port)) { + LogDebug("[Server] Starts to listen to port %d", port); + return true; + } else { + LogDebug("[Server] Cannot listen to port %d", port); + return false; + } + }; + + if (port_ == 0) { + int attempts = 1000; + int port = 0; + bool res = false; + + do { + --attempts; + port = 10000 + (rand() % 55000); // NOLINT + res = listen(port); + } while (run_ == true && res == false && attempts != 0); + + if (res) { + port_ = port; + hub_.run(); // Blocks until done + } else if (attempts == 0) { + LogError("Failed to find free port after 1000 attempts"); + } + } else { + if (listen(port_) == true) { + hub_.run(); // Blocks until done + } else { + LogWarning("[Server] Cannot listen to port %d", port_.load()); + } + } + + run_ = false; + }); + } + + void Stop() { + run_ = false; + + if (keep_alive_) { + keep_alive_->send(); + keep_alive_ = nullptr; + } + + if (thread_ != nullptr) { + thread_->join(); + delete thread_; + thread_ = nullptr; + } + } + + int GetPort(bool waitForPort = false) const { + while (waitForPort == true && run_ == true && port_ == 0) { + firebase::internal::Sleep(10); + } + + return port_; + } + + private: + std::atomic port_; + std::atomic run_; // Is the listen thread started and running + uWS::Hub hub_; + std::thread* thread_; + uS::Async* keep_alive_; +}; + +std::string GetLocalHostUri(int port) { + std::stringstream ss; + ss << "ws://localhost:" << port; + return ss.str(); +} + +class TestClientEventHandler : public WebSocketClientEventHandler { + public: + explicit TestClientEventHandler(Semaphore* s) + : is_connected_(false), + is_msg_received_(false), + msg_received_(), + is_closed_(false), + is_error_(false), + semaphore_(s) {} + ~TestClientEventHandler() override{}; + + void OnOpen() override { + is_connected_ = true; + semaphore_->Post(); + } + + void OnMessage(const char* msg) override { + is_msg_received_ = true; + msg_received_ = msg; + semaphore_->Post(); + } + + void OnClose() override { + is_closed_ = true; + semaphore_->Post(); + } + + void OnError(const WebSocketClientErrorData& error_data) override { + is_error_ = true; + semaphore_->Post(); + } + + bool is_connected_ = false; + bool is_msg_received_ = false; + std::string msg_received_; + bool is_closed_ = false; + bool is_error_ = false; + + private: + Semaphore* semaphore_; +}; + +// Test if the client can connect to a local echo server, send a message, +// receive message and close the connection properly. +TEST(WebSocketClientImpl, Test1) { + // Launch a local echo server + TestWebSocketEchoServer server(0); + server.Start(); + + auto uri = GetLocalHostUri(server.GetPort(true)); + + Semaphore semaphore(1); + TestClientEventHandler handler(&semaphore); + Logger logger(nullptr); + scheduler::Scheduler scheduler; + WebSocketClientImpl ws_client(uri.c_str(), "", &logger, &scheduler, &handler); + + // Connect to local server + LogDebug("[Client] Connecting to %s", uri.c_str()); + EXPECT_TRUE(semaphore.TryWait()); + ws_client.Connect(5000); + semaphore.Wait(); + semaphore.Post(); + EXPECT_TRUE(handler.is_connected_ && !handler.is_error_); + + // Send a message and wait for the response + EXPECT_TRUE(semaphore.TryWait()); + ws_client.Send("Hello World"); + semaphore.Wait(); + semaphore.Post(); + EXPECT_TRUE(handler.is_msg_received_ && !handler.is_error_); + EXPECT_STREQ("Hello World", handler.msg_received_.c_str()); + + // Close the connection + EXPECT_TRUE(semaphore.TryWait()); + ws_client.Close(); + semaphore.Wait(); + semaphore.Post(); + EXPECT_TRUE(handler.is_closed_ && !handler.is_error_); + + // Stop the server + server.Stop(); +} + +// Test if it is safe to create the client and destroy it immediately. +// This is to test if the destructor can properly end the event loop. +// Otherwise, it would block forever and timeout +TEST(WebSocketClientImpl, TestEdgeCase1) { + Logger logger(nullptr); + scheduler::Scheduler scheduler; + WebSocketClientImpl ws_client("ws://localhost", "", &logger, &scheduler); +} + +// Test if it is safe to connect to a server and destroy the client immediately. +// This is to test if the destructor can properly end the event loop +// Otherwise, it would block forever and timeout +TEST(WebSocketClientImpl, TestEdgeCase2) { + // Launch a local echo server + TestWebSocketEchoServer server(0); + server.Start(); + Logger logger(nullptr); + scheduler::Scheduler scheduler; + + auto uri = GetLocalHostUri(server.GetPort(true)); + + int count = 0; + while ((count++) < 10000) { + WebSocketClientImpl* ws_client = + new WebSocketClientImpl(uri.c_str(), "", &logger, &scheduler); + + // Connect to local server + LogDebug("[Client][%d] Connecting to %s", count, uri.c_str()); + ws_client->Connect(5000); + + // Immediately destroy the client right after connect request + delete ws_client; + } + + // Stop the server + server.Stop(); +} + +} // namespace connection +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/cache_policy_test.cc b/database/tests/desktop/core/cache_policy_test.cc new file mode 100644 index 0000000000..a21e8d5493 --- /dev/null +++ b/database/tests/desktop/core/cache_policy_test.cc @@ -0,0 +1,70 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/cache_policy.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +namespace { + +TEST(LRUCachePolicy, ShouldPrune) { + const uint64_t kMaxSizeBytes = 1000; + LRUCachePolicy cache_policy(kMaxSizeBytes); + + uint64_t queries_to_keep = cache_policy.GetMaxNumberOfQueriesToKeep(); + EXPECT_EQ(queries_to_keep, 1000); + + // Should prune if the current number of bytes exceeds the max number of + // bytes. + EXPECT_TRUE(cache_policy.ShouldPrune(2000, 0)); + // Should prune if the number of prunable queries is greater than the maximum + // number of prunable queries (defined in the LRUCachePolicy implementation). + EXPECT_TRUE(cache_policy.ShouldPrune(0, 2000)); + // Should prune if both of the above are true. + EXPECT_TRUE(cache_policy.ShouldPrune(2000, 2000)); + + // Should not prune if at least one of the above conditions is not met. + EXPECT_FALSE(cache_policy.ShouldPrune(0, 0)); +} + +TEST(LRUCachePolicy, ShouldCheckCacheSize) { + const uint64_t kMaxSizeBytes = 1000; + LRUCachePolicy cache_policy(kMaxSizeBytes); + + // Should check cache cize of the number of server updates is greater than + // number of server updates between cache checks (defined in the + // LRUCachePolicy implementation). + EXPECT_TRUE(cache_policy.ShouldCheckCacheSize(2000)); + EXPECT_TRUE(cache_policy.ShouldCheckCacheSize(1001)); + EXPECT_FALSE(cache_policy.ShouldCheckCacheSize(1000)); + EXPECT_FALSE(cache_policy.ShouldCheckCacheSize(500)); +} + +TEST(LRUCachePolicy, GetPercentOfQueriesToPruneAtOnce) { + const uint64_t kMaxSizeBytes = 1000; + LRUCachePolicy cache_policy(kMaxSizeBytes); + + // This should be exactly 20%. + EXPECT_EQ(cache_policy.GetPercentOfQueriesToPruneAtOnce(), .2); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/compound_write_test.cc b/database/tests/desktop/core/compound_write_test.cc new file mode 100644 index 0000000000..435f2b7291 --- /dev/null +++ b/database/tests/desktop/core/compound_write_test.cc @@ -0,0 +1,545 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/compound_write.h" + +#include +#include + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(CompoundWrite, CompoundWrite) { + { + CompoundWrite write; + EXPECT_TRUE(write.IsEmpty()); + EXPECT_TRUE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.GetRootWrite().has_value()); + } + { + CompoundWrite write = CompoundWrite::EmptyWrite(); + EXPECT_TRUE(write.IsEmpty()); + EXPECT_TRUE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.GetRootWrite().has_value()); + } +} + +TEST(CompoundWrite, FromChildMerge) { + { + const std::map& merge{ + std::make_pair("", 0), + }; + CompoundWrite write = CompoundWrite::FromChildMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_EQ(*write.GetRootWrite(), 0); + } + { + const std::map& merge{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc/ddd", 3), + std::make_pair("ccc/eee", 4), + }; + CompoundWrite write = CompoundWrite::FromChildMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.write_tree().value().has_value()); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(write.write_tree().GetValueAt(Path("zzz")), nullptr); + } +} + +TEST(CompoundWrite, FromVariantMerge) { + { + Variant merge(std::map{ + std::make_pair("", 0), + }); + CompoundWrite write = CompoundWrite::FromVariantMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_EQ(*write.GetRootWrite(), 0); + } + { + Variant merge(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc/ddd", 3), + std::make_pair("ccc/eee", 4), + }); + CompoundWrite write = CompoundWrite::FromVariantMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.write_tree().value().has_value()); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(write.write_tree().GetValueAt(Path("zzz")), nullptr); + } +} + +TEST(CompoundWrite, FromPathMerge) { + { + const std::map& merge{ + std::make_pair(Path(""), 0), + }; + + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_EQ(*write.GetRootWrite(), 0); + } + { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.write_tree().value().has_value()); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(write.write_tree().GetValueAt(Path("zzz")), nullptr); + } +} + +// This just replicates the set up work done in the FromPathMerge test. +class CompoundWriteTest : public ::testing::Test { + void SetUp() override { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })), + }; + + write_ = CompoundWrite::FromPathMerge(merge); + } + + void TearDown() override {} + + protected: + CompoundWrite write_; +}; + +TEST_F(CompoundWriteTest, EmptyWrite) { + CompoundWrite empty = CompoundWrite::EmptyWrite(); + EXPECT_TRUE(empty.IsEmpty()); +} + +TEST_F(CompoundWriteTest, AddWriteEmptyPath) { + CompoundWrite new_write = write_.AddWrite(Path(), Optional(100)); + + // New write should just be the root value. + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("bbb")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/ddd")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/eee")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/fff")), nullptr); + EXPECT_TRUE(new_write.write_tree().value().has_value()); + EXPECT_EQ(new_write.write_tree().value().value(), 100); +} + +TEST_F(CompoundWriteTest, AddWriteInlineEmptyPath) { + write_.AddWriteInline(Path(), Optional(100)); + CompoundWrite& new_write = write_; + + // New write should just be the root value. + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("bbb")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/ddd")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/eee")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/fff")), nullptr); + EXPECT_TRUE(new_write.write_tree().value().has_value()); + EXPECT_EQ(new_write.write_tree().value().value(), 100); +} + +TEST_F(CompoundWriteTest, AddWritePriorityWrite) { + { + CompoundWrite new_write = + write_.AddWrite(Path("ccc/.priority"), Optional(100)); + + // Everything should be the same, but with an additional .priority field. + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/fff")), + Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/.priority")), 100); + } + + { + CompoundWrite new_write = + write_.AddWrite(Path("aaa/bad_path/.priority"), Optional(100)); + + // New write should be identical to the old write. + EXPECT_EQ(new_write, write_); + } +} + +TEST_F(CompoundWriteTest, AddWriteThatDoesNotOverwrite) { + CompoundWrite new_write = + write_.AddWrite(Path("iii/jjj"), Optional(100)); + + // New write should have the new value alongside old values. + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/fff")), + Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("iii/jjj")), 100); +} + +TEST_F(CompoundWriteTest, AddWriteThatShadowsExistingData) { + CompoundWrite new_write = + write_.AddWrite(Path("ccc/fff/ggg"), Optional(100)); + + // Values being shadowed are still part of the CompoundWrite. + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/fff")), + Variant(std::map{ + std::make_pair("ggg", 100), + std::make_pair("hhh", 6), + })); +} + +TEST_F(CompoundWriteTest, AddWrites) { + const std::map& second_merge{ + std::make_pair(Path("zzz"), -1), + std::make_pair(Path("yyy"), -2), + std::make_pair(Path("xxx/www"), -3), + std::make_pair(Path("xxx/vvv"), -4), + }; + CompoundWrite second_write = CompoundWrite::FromPathMerge(second_merge); + + const std::map& third_merge{ + std::make_pair(Path("apple"), 1111), + std::make_pair(Path("banana"), 2222), + std::make_pair(Path("carrot/date"), 3333), + std::make_pair(Path("carrot/eggplant"), 4444), + }; + CompoundWrite third_write = CompoundWrite::FromPathMerge(third_merge); + + CompoundWrite updated_write = write_.AddWrites(Path(), second_write); + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); + + updated_write = updated_write.AddWrites(Path("ccc"), third_write); + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/apple")), 1111); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/banana")), 2222); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/date")), + 3333); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/eggplant")), + 4444); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); +} + +TEST_F(CompoundWriteTest, AddWritesInline) { + const std::map& second_merge{ + std::make_pair(Path("zzz"), -1), + std::make_pair(Path("yyy"), -2), + std::make_pair(Path("xxx/www"), -3), + std::make_pair(Path("xxx/vvv"), -4), + }; + CompoundWrite second_write = CompoundWrite::FromPathMerge(second_merge); + + const std::map& third_merge{ + std::make_pair(Path("apple"), 1111), + std::make_pair(Path("banana"), 2222), + std::make_pair(Path("carrot/date"), 3333), + std::make_pair(Path("carrot/eggplant"), 4444), + }; + CompoundWrite third_write = CompoundWrite::FromPathMerge(third_merge); + + write_.AddWritesInline(Path(), second_write); + CompoundWrite& updated_write = write_; + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); + + updated_write.AddWritesInline(Path("ccc"), third_write); + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/apple")), 1111); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/banana")), 2222); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/date")), + 3333); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/eggplant")), + 4444); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); +} + +TEST_F(CompoundWriteTest, RemoveWrite) { + CompoundWrite new_write = write_.RemoveWrite(Path("aaa")); + + // New write should be missing aaa + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); +} + +TEST_F(CompoundWriteTest, RemoveWriteInline) { + write_.RemoveWriteInline(Path("aaa")); + CompoundWrite& new_write = write_; + + // New write should be missing aaa + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); +} + +TEST_F(CompoundWriteTest, HasCompleteWrite) { + EXPECT_TRUE(write_.HasCompleteWrite(Path("aaa"))); + EXPECT_TRUE(write_.HasCompleteWrite(Path("bbb"))); + EXPECT_FALSE(write_.HasCompleteWrite(Path("ccc"))); + EXPECT_TRUE(write_.HasCompleteWrite(Path("ccc/ddd"))); + EXPECT_TRUE(write_.HasCompleteWrite(Path("ccc/eee"))); + EXPECT_FALSE(write_.HasCompleteWrite(Path("zzz"))); +} + +TEST_F(CompoundWriteTest, GetRootWriteEmpty) { + Optional root = write_.GetRootWrite(); + EXPECT_FALSE(root.has_value()); +} + +TEST(CompoundWrite, GetRootWritePopulated) { + const std::map& merge{ + std::make_pair(Path(""), "One billion"), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + Optional root = write.GetRootWrite(); + EXPECT_TRUE(root.has_value()); + EXPECT_EQ(root.value(), "One billion"); +} + +TEST_F(CompoundWriteTest, GetCompleteVariant) { + EXPECT_FALSE(write_.GetCompleteVariant(Path()).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("aaa")).value(), 1); + EXPECT_EQ(write_.GetCompleteVariant(Path("bbb")).value(), 2); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/ddd")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/ddd")).value(), 3); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/eee")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/eee")).value(), 4); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/fff/ggg")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/fff/ggg")).value(), 5); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/fff/ggg")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/fff/hhh")).value(), 6); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/fff/iii")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/fff/iii")).value(), + Variant::Null()); + EXPECT_FALSE(write_.GetCompleteVariant(Path("zzz")).has_value()); +} + +TEST_F(CompoundWriteTest, GetCompleteChildren) { + std::vector> children = + write_.GetCompleteChildren(); + + std::vector> expected_children = { + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + }; + + EXPECT_THAT(children, Pointwise(Eq(), expected_children)); +} + +TEST_F(CompoundWriteTest, ChildCompoundWriteEmptyPath) { + CompoundWrite child = write_.ChildCompoundWrite(Path()); + + // Should be exactly the same as write_. + EXPECT_FALSE(child.IsEmpty()); + EXPECT_FALSE(child.write_tree().IsEmpty()); + EXPECT_FALSE(child.write_tree().value().has_value()); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(child.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(child.write_tree().GetValueAt(Path("zzz")), nullptr); +} + +TEST(CompoundWrite, ChildCompoundWriteShadowingWrite) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), -9999), std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + CompoundWrite child = write.ChildCompoundWrite(Path("ccc")); + EXPECT_EQ(child.GetRootWrite().value(), -9999); +} + +TEST_F(CompoundWriteTest, ChildCompoundWriteNonShadowingWrite) { + CompoundWrite child = write_.ChildCompoundWrite(Path("ccc")); + + EXPECT_FALSE(child.IsEmpty()); + EXPECT_FALSE(child.write_tree().IsEmpty()); + EXPECT_FALSE(child.write_tree().value().has_value()); + EXPECT_EQ(child.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(child.write_tree().GetValueAt(Path("bbb")), nullptr); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("ddd")), 3); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("eee")), 4); + EXPECT_EQ(child.write_tree().GetValueAt(Path("zzz")), nullptr); +} + +TEST_F(CompoundWriteTest, ChildCompoundWrites) { + std::map writes = write_.ChildCompoundWrites(); + + CompoundWrite& aaa = writes["aaa"]; + CompoundWrite& bbb = writes["bbb"]; + CompoundWrite& ccc = writes["ccc"]; + + EXPECT_EQ(writes.size(), 3); + EXPECT_EQ(aaa.write_tree().value().value(), 1); + EXPECT_EQ(bbb.write_tree().value().value(), 2); + EXPECT_EQ(*ccc.write_tree().GetValueAt(Path("ddd")), 3); + EXPECT_EQ(*ccc.write_tree().GetValueAt(Path("eee")), 4); +} + +TEST_F(CompoundWriteTest, IsEmpty) { + CompoundWrite compound_write; + EXPECT_TRUE(compound_write.IsEmpty()); + + CompoundWrite empty = CompoundWrite::EmptyWrite(); + EXPECT_TRUE(empty.IsEmpty()); + + CompoundWrite add_write = compound_write.AddWrite(Path(), 100); + EXPECT_TRUE(compound_write.IsEmpty()); + EXPECT_FALSE(add_write.IsEmpty()); +} + +TEST_F(CompoundWriteTest, Apply) { + Variant expected_variant(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + }), + }), + std::make_pair("zzz", 100), + }); + Variant variant_to_apply(std::map{ + std::make_pair("zzz", 100), + }); + + EXPECT_EQ(write_.Apply(variant_to_apply), expected_variant); +} + +TEST_F(CompoundWriteTest, Equality) { + const std::map& same_merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })), + }; + CompoundWrite same_write = CompoundWrite::FromPathMerge(same_merge); + const std::map& different_merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 100), + })), + }; + CompoundWrite different_write = CompoundWrite::FromPathMerge(different_merge); + + EXPECT_EQ(write_, same_write); + EXPECT_NE(write_, different_write); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/event_registration_test.cc b/database/tests/desktop/core/event_registration_test.cc new file mode 100644 index 0000000000..45140db71e --- /dev/null +++ b/database/tests/desktop/core/event_registration_test.cc @@ -0,0 +1,186 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/event_registration.h" + +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/desktop/database_desktop.h" +#include "database/src/desktop/database_reference_desktop.h" +#include "database/src/desktop/view/change.h" +#include "database/src/desktop/view/event.h" +#include "database/src/desktop/view/event_type.h" +#include "database/src/include/firebase/database/common.h" +#include "database/tests/desktop/test/mock_listener.h" +#include "firebase/database/common.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::_; +using ::testing::StrEq; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(ValueEventRegistrationTest, RespondsTo) { + ValueEventRegistration registration(nullptr, nullptr, QuerySpec()); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildRemoved)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildAdded)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildMoved)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildChanged)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeValue)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeError)); +} + +TEST(ValueEventRegistrationTest, CreateEvent) { + ValueEventRegistration registration(nullptr, nullptr, QuerySpec()); + Variant variant = std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 200), + }; + IndexedVariant change_variant(variant, QueryParams()); + Change change(kEventTypeValue, change_variant, "new"); + QuerySpec query_spec; + query_spec.path = Path("change/path"); + Event event = registration.GenerateEvent(change, query_spec); + EXPECT_EQ(event.type, kEventTypeValue); + EXPECT_EQ(event.event_registration, ®istration); + EXPECT_EQ(event.snapshot->GetValue().int64_value(), 100); + EXPECT_EQ(event.snapshot->GetPriority().int64_value(), 200); + EXPECT_EQ(event.snapshot->path(), Path("change/path/new")); + EXPECT_STREQ(event.prev_name.c_str(), ""); + EXPECT_EQ(event.error, kErrorNone); + EXPECT_EQ(event.path, Path()); +} + +TEST(ValueEventRegistrationTest, FireEvent) { + MockValueListener listener; + ValueEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeValue, ®istration, snapshot); + EXPECT_CALL(listener, OnValueChanged(_)); + registration.FireEvent(event); +} + +TEST(ValueEventRegistrationTest, FireEventCancel) { + MockValueListener listener; + ValueEventRegistration registration(nullptr, &listener, QuerySpec()); + EXPECT_CALL(listener, OnCancelled(kErrorDisconnected, _)); + registration.FireCancelEvent(kErrorDisconnected); +} + +TEST(ValueEventRegistrationTest, MatchesListener) { + MockValueListener right_listener; + MockValueListener wrong_listener; + MockChildListener wrong_type_listener; + ValueEventRegistration registration(nullptr, &right_listener, QuerySpec()); + EXPECT_TRUE(registration.MatchesListener(&right_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_type_listener)); +} + +TEST(ChildEventRegistrationTest, RespondsTo) { + ChildEventRegistration registration(nullptr, nullptr, QuerySpec()); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildRemoved)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildAdded)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildMoved)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildChanged)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeValue)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeError)); +} + +TEST(ChildEventRegistrationTest, CreateEvent) { + ChildEventRegistration registration(nullptr, nullptr, QuerySpec()); + Variant variant = std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 200), + }; + IndexedVariant change_variant(variant, QueryParams()); + Change change(kEventTypeChildAdded, change_variant, "new"); + QuerySpec query_spec; + query_spec.path = Path("change/path"); + Event event = registration.GenerateEvent(change, query_spec); + EXPECT_EQ(event.type, kEventTypeChildAdded); + EXPECT_EQ(event.event_registration, ®istration); + EXPECT_EQ(event.snapshot->GetValue().int64_value(), 100); + EXPECT_EQ(event.snapshot->GetPriority().int64_value(), 200); + EXPECT_EQ(event.snapshot->path(), Path("change/path/new")); + EXPECT_STREQ(event.prev_name.c_str(), ""); + EXPECT_EQ(event.error, kErrorNone); + EXPECT_EQ(event.path, Path()); +} + +TEST(ChildEventRegistrationTest, FireChildAddedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildAdded, ®istration, snapshot, + "Apples and bananas"); + EXPECT_CALL(listener, OnChildAdded(_, StrEq("Apples and bananas"))); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireChildChangedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildChanged, ®istration, snapshot, + "Upples and banunus"); + EXPECT_CALL(listener, OnChildChanged(_, StrEq("Upples and banunus"))); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireChildMovedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildMoved, ®istration, snapshot, + "Epples and banenes"); + EXPECT_CALL(listener, OnChildMoved(_, StrEq("Epples and banenes"))); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireChildRemovedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildRemoved, ®istration, snapshot); + EXPECT_CALL(listener, OnChildRemoved(_)); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireEventCancel) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + EXPECT_CALL(listener, OnCancelled(kErrorDisconnected, _)); + registration.FireCancelEvent(kErrorDisconnected); +} + +TEST(ChildEventRegistrationTest, MatchesListener) { + MockChildListener right_listener; + MockChildListener wrong_listener; + MockValueListener wrong_type_listener; + ChildEventRegistration registration(nullptr, &right_listener, QuerySpec()); + EXPECT_TRUE(registration.MatchesListener(&right_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_type_listener)); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/indexed_variant_test.cc b/database/tests/desktop/core/indexed_variant_test.cc new file mode 100644 index 0000000000..5da86b9ba2 --- /dev/null +++ b/database/tests/desktop/core/indexed_variant_test.cc @@ -0,0 +1,677 @@ +// Copyright 2018 Google LLC +// +// 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 + +#include "app/memory/unique_ptr.h" +#include "app/src/variant_util.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "database/src/desktop/util_desktop.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::Ne; + +namespace firebase { +namespace database { +namespace internal { + +typedef std::vector> TestList; + +// Hardcoded Json string for test uses \' instead of \" for readability. +// This utility function converts \' into \" +std::string& ConvertQuote(std::string* in) { + std::replace(in->begin(), in->end(), '\'', '\"'); + return *in; +} + +// Test for ConvertQuote +TEST(IndexedVariantHelperFunction, ConvertQuote) { + { + std::string test_string = ""; + EXPECT_THAT(ConvertQuote(&test_string), Eq("")); + } + { + std::string test_string = "'"; + EXPECT_THAT(ConvertQuote(&test_string), Eq("\"")); + } + { + std::string test_string = "\""; + EXPECT_THAT(ConvertQuote(&test_string), Eq("\"")); + } + { + std::string test_string = "''"; + EXPECT_THAT(ConvertQuote(&test_string), Eq("\"\"")); + } + { + std::string test_string = "{'A':'a'}"; + EXPECT_THAT(ConvertQuote(&test_string), Eq("{\"A\":\"a\"}")); + } +} + +std::string QueryParamsToString(const QueryParams& params) { + std::stringstream ss; + + ss << "{ order_by="; + switch (params.order_by) { + case QueryParams::kOrderByPriority: + ss << "kOrderByPriority"; + break; + case QueryParams::kOrderByKey: + ss << "kOrderByKey"; + break; + case QueryParams::kOrderByValue: + ss << "kOrderByValue"; + break; + case QueryParams::kOrderByChild: + ss << "kOrderByChild(" << params.order_by_child << ")"; + break; + } + + if (!params.equal_to_value.is_null()) { + ss << ", equal_to_value=" << util::VariantToJson(params.equal_to_value); + } + if (!params.equal_to_child_key.empty()) { + ss << ", equal_to_child_key=" << params.equal_to_child_key; + } + if (!params.start_at_value.is_null()) { + ss << ", start_at_value=" << util::VariantToJson(params.start_at_value); + } + if (!params.start_at_child_key.empty()) { + ss << ", start_at_child_key=" << params.start_at_child_key; + } + if (!params.end_at_value.is_null()) { + ss << ", end_at_value=" << util::VariantToJson(params.end_at_value); + } + if (!params.end_at_child_key.empty()) { + ss << ", end_at_child_key=" << params.end_at_child_key; + } + if (params.limit_first != 0) { + ss << ", limit_first=" << params.limit_first; + } + if (params.limit_last != 0) { + ss << ", limit_last=" << params.limit_last; + } + ss << " }"; + + return ss.str(); +} + +// Validate the index created by IndexedVariant and its order +void VerifyIndex(const Variant* input_variant, + const QueryParams* input_query_params, TestList* expected) { + // IndexedVariant supports 4 types of constructor: + // IndexedVariant() - both input_variant and input_query_params are null + // IndexedVariant(Variant) - only input_query_params is null + // IndexedVariant(Variant, QueryParams) - both are NOT null + // Additionally, we test the copy constructor in all cases + // IndexedVariant(IndexedVariant) - A copy of an existing IndexedVariant + UniquePtr index_variant; + if (input_variant == nullptr && input_query_params == nullptr) { + index_variant = MakeUnique(); + } else if (input_variant != nullptr && input_query_params == nullptr) { + index_variant = MakeUnique(*input_variant); + } else if (input_variant != nullptr && input_query_params != nullptr) { + index_variant = + MakeUnique(*input_variant, *input_query_params); + } + + // assert if input_variant is null but input_query_params is not null + assert(index_variant); + IndexedVariant copied_index_variant(*index_variant); + + const IndexedVariant::Index* indexes[] = { + &index_variant->index(), + &copied_index_variant.index(), + }; + for (const auto& index : indexes) { + // Convert TestList::index() into TestList for comparison + TestList actual_list; + for (auto& it : *index) { + actual_list.push_back( + {it.first.AsString().string_value(), util::VariantToJson(it.second)}); + } + + for (auto& it : *expected) { + // Make sure Json string is formatted in the same way since we are doing + // string comparison. + ConvertQuote(&it.second); + it.second = util::VariantToJson(util::JsonToVariant(it.second.c_str())); + } + + EXPECT_THAT(actual_list, Eq(*expected)) + << "Test Variant: " << util::VariantToJson(*input_variant) << std::endl + << "Test QueryParams: " + << (input_query_params ? QueryParamsToString(*input_query_params) + : "null"); + } +} + +// Default IndexedVariant +TEST(IndexedVariant, ConstructorTestBasic) { + TestList expected_result = {}; + VerifyIndex(nullptr, nullptr, &expected_result); +} + +TEST(IndexedVariant, ConstructorTestDefaultQueryParamsNoPriority) { + { + Variant test_input = Variant::Null(); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(123); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(123.456); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(true); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(false); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = "[1,2,3]"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = "{}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = + "{" + " 'A': 1," + " 'B': 'b'," + " 'C':true" + "}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = { + {"A", "1"}, + {"B", "'b'"}, + {"C", "true"}, + }; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = + "{" + " 'A': 1," + " 'B': { '.value': 'b', '.priority': 100 }," + " 'C': true" + "}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = { + {"A", "1"}, + {"C", "true"}, + {"B", "{ '.value': 'b', '.priority': 100 }"}, + }; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = + "{" + " 'A': { '.value': 1, '.priority': 300 }," + " 'B': { '.value': 'b', '.priority': 100 }," + " 'C': { '.value': true, '.priority': 200 }" + "}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = { + {"B", "{ '.value': 'b', '.priority': 100 }"}, + {"C", "{ '.value': true, '.priority': 200 }"}, + {"A", "{ '.value': 1, '.priority': 300 }"}, + }; + VerifyIndex(&test_input, nullptr, &expected_result); + } +} + +// Used to run individual test for GetOrderByVariantTest +// Need to access private function IndexedVariant::GetOrderByVariant(). +// Therefore this class is friended by IndexedVariant +class IndexedVariantGetOrderByVariantTest : public ::testing::Test { + protected: + void RunTest(const QueryParams& params, const Variant& key, + const TestList& value_result_list, const char* test_name) { + IndexedVariant indexed_variant(Variant::Null(), params); + + for (auto& test : value_result_list) { + std::string value_string = test.first; + Variant value = util::JsonToVariant(ConvertQuote(&value_string).c_str()); + bool expected_null = test.second.empty(); + std::string expected_string = test.second; + Variant expected = + util::JsonToVariant(ConvertQuote(&expected_string).c_str()); + + auto* result = indexed_variant.GetOrderByVariant(key, value); + EXPECT_THAT(!result || result->is_null(), Eq(expected_null)) + << test_name << " (" << key.AsString().string_value() << ", " + << value_string << ") "; + if (!expected_null && result != nullptr) { + EXPECT_THAT(*result, Eq(expected)) + << test_name << " (" << key.AsString().string_value() << ", " + << value_string << ") "; + } + } + } +}; + +TEST_F(IndexedVariantGetOrderByVariantTest, GetOrderByVariantTest) { + // Test order by priority + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", ""}, + {"{'.value': 1, '.priority': 100}", "100"}, + {"{'B': 1,'.priority': 100}", "100"}, + {"{'B': {'.value': 1, '.priority': 200} ,'.priority': 100}", "100"}, + {"{'B': {'C': 1, '.priority': 200} ,'.priority': 100}", "100"}, + }; + + RunTest(params, key, value_result_list, "OrderByPriority"); + } + + // Test order by key + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", "'A'"}, + {"{'.value': 1, '.priority': 100}", "'A'"}, + {"{'B': 1,'.priority': 100}", "'A'"}, + {"{'B': {'.value': 1, '.priority': 200} ,'.priority': 100}", "'A'"}, + {"{'B': {'C': 1, '.priority': 200} ,'.priority': 100}", "'A'"}, + }; + + RunTest(params, key, value_result_list, "OrderByKey"); + } + + // Test order by value + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + IndexedVariant indexed_variant(Variant::Null(), params); + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", "1"}, + {"{'.value': 1, '.priority': 100}", "1"}, + }; + + RunTest(params, key, value_result_list, "OrderByValue"); + } + + // Test order by child + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "B"; + IndexedVariant indexed_variant(Variant::Null(), params); + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", ""}, + {"{'.value': 1, '.priority': 100}", ""}, + {"{'B': 1,'.priority': 100}", "1"}, + {"{'B': {'.value': 1, '.priority': 200} ,'.priority': 100}", "1"}, + }; + + RunTest(params, key, value_result_list, "OrderByChild"); + } +} + +TEST(IndexedVariant, FindTest) { + std::string test_data = + "{" + " 'A': 1," + " 'B': 'b'," + " 'C': true" + "}"; + Variant variant = util::JsonToVariant(ConvertQuote(&test_data).c_str()); + + IndexedVariant indexed_variant(variant); + + // List of test: { key, expected} + TestList test_list = { + {"A", "A"}, + {"B", "B"}, + {"C", "C"}, + {"D", ""}, + }; + + for (auto& test : test_list) { + auto it = indexed_variant.Find(Variant(test.first)); + + bool expected_found = !test.second.empty(); + EXPECT_THAT(it != indexed_variant.index().end(), Eq(expected_found)) + << "Find(" << test.first << ")"; + + if (expected_found && it != indexed_variant.index().end()) { + EXPECT_THAT(it->first, Eq(Variant(test.second))) + << "Find(" << test.first << ")"; + } + } +} + +TEST(IndexedVariant, GetPredecessorChildNameTest) { + std::string test_data = + "{" + " 'A': { '.value': 1, '.priority': 300 }," + " 'B': { '.value': 'b', '.priority': 100 }," + " 'C': { '.value': true, '.priority': 200 }," + " 'D': { 'E': {'.value': 'e', '.priority': 200}, '.priority': 100 }" + "}"; + + // Expected Order (Order by priority by default) + // ["B", { ".value": "b", ".priority": 100 } ], + // ["D", { "E" : {".value": "e", ".priority": 200 }, ".priority": 100 } ], + // ["C", { ".value": true, ".priority": 200 } ], + // ["A", { ".value": 1, ".priority": 300 } ] + Variant variant = util::JsonToVariant(ConvertQuote(&test_data).c_str()); + + // Use default QueryParams which uses OrderByPriority + IndexedVariant indexed_variant(variant); + + struct TestCase { + // Input key string + std::string key; + + // Input value variant, structured in Json string, with \" replaced for + // readibility. + std::string value; + + // Expected return value from GetPredecessorChildName(). If it is empty + // string (""), then the expected return value is nullptr + std::string expected_result; + }; + + std::vector test_list = { + {"A", "{ '.value': 1, '.priority': 300 }", "C"}, + // The first element, no predecessor + {"B", "{ '.value': 'b', '.priority': 100 }", ""}, + {"C", "{ '.value': true, '.priority': 200 }", "D"}, + {"D", "{ 'E': {'.value': 'e', '.priority': 200}, '.priority': 100 }", + "B"}, + // Pair not found + {"E", "'e'", ""}, + // EXCEPTION: Not found due to missing priority. + {"A", "1", ""}, + {"B", "'b'", ""}, + {"C", "true", ""}, + {"D", "{ 'E': {'.value': 'e', '.priority': 200}}", ""}, + {"D", "{ 'E': 'e'}}", ""}, + // EXCEPTION: Not found because priority is different + {"A", "{ '.value': 1, '.priority': 1000 }", ""}, + // EXCEPTION: Found because, even though the value is different, the + // priority is the same. + {"A", "{ '.value': 'a', '.priority': 300 }", "C"}, + }; + + for (auto& test : test_list) { + std::string& key = test.key; + Variant value = util::JsonToVariant(ConvertQuote(&test.value).c_str()); + const char* child_name = + indexed_variant.GetPredecessorChildName(key, value); + + std::string& expected = test.expected_result; + bool expected_found = !expected.empty(); + EXPECT_THAT(child_name != nullptr, Eq(expected_found)) + << "GetPredecessorChildNameTest(" << key << ", " << test.value << ")"; + + if (expected_found && child_name != nullptr) { + EXPECT_THAT(std::string(child_name), Eq(expected)) + << "GetPredecessorChildNameTest(" << key << ", " << test.value << ")"; + } + } +} + +TEST(IndexedVariant, Variant) { + Variant variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + QueryParams params; + IndexedVariant indexed_variant(variant, params); + Variant expected = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + EXPECT_EQ(indexed_variant.variant(), expected); +} + +TEST(IndexedVariant, UpdateChildTest) { + Variant variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + + IndexedVariant indexed_variant(variant); + + // Add new element. + IndexedVariant result1 = indexed_variant.UpdateChild("eee", 500); + // Change existing element. + IndexedVariant result2 = indexed_variant.UpdateChild("ccc", 600); + // Remove existing element. + IndexedVariant result3 = indexed_variant.UpdateChild("bbb", Variant::Null()); + + Variant expected1 = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + Variant expected2 = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 600), + std::make_pair("ddd", 400), + }; + Variant expected3 = std::map{ + std::make_pair("aaa", 100), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + EXPECT_EQ(result1.variant(), expected1); + EXPECT_EQ(result2.variant(), expected2); + EXPECT_EQ(result3.variant(), expected3); +} + +TEST(IndexedVariant, UpdatePriorityTest) { + Variant variant = 100; + IndexedVariant indexed_variant(variant); + + IndexedVariant result = indexed_variant.UpdatePriority(1234); + Variant expected = std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1234), + }; + + EXPECT_EQ(result.variant(), expected); +} + +TEST(IndexedVariant, GetFirstAndLastChildByPriority) { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + Variant variant = std::map{ + std::make_pair("aaa", + std::map{std::make_pair(".priority", 3), + std::make_pair(".value", 100)}), + std::make_pair("bbb", + std::map{std::make_pair(".priority", 4), + std::make_pair(".value", 200)}), + std::make_pair("ccc", + std::map{std::make_pair(".priority", 1), + std::make_pair(".value", 300)}), + std::make_pair("ddd", + std::map{std::make_pair(".priority", 2), + std::make_pair(".value", 400)}), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 1), + std::make_pair(".value", 300)})); + Optional> expected_last(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 4), + std::make_pair(".value", 200)})); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildByChild) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "zzz"; + Variant variant = std::map{ + std::make_pair("aaa", + std::map{std::make_pair("zzz", 2)}), + std::make_pair("bbb", + std::map{std::make_pair("zzz", 1)}), + std::make_pair("ccc", + std::map{std::make_pair("zzz", 4)}), + std::make_pair("ddd", + std::map{std::make_pair("zzz", 3)}), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first(std::make_pair( + "bbb", std::map{std::make_pair("zzz", 1)})); + Optional> expected_last(std::make_pair( + "ccc", std::map{std::make_pair("zzz", 4)})); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildByKey) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + Variant variant = std::map{ + std::make_pair("aaa", 400), + std::make_pair("bbb", 300), + std::make_pair("ccc", 200), + std::make_pair("ddd", 100), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first( + std::make_pair("aaa", 400)); + Optional> expected_last( + std::make_pair("ddd", 100)); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildByValue) { + // Value + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + Variant variant = std::map{ + std::make_pair("aaa", 400), + std::make_pair("bbb", 300), + std::make_pair("ccc", 200), + std::make_pair("ddd", 100), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first( + std::make_pair("ddd", 100)); + Optional> expected_last( + std::make_pair("aaa", 400)); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildLeaf) { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + Variant variant = 1000; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first; + Optional> expected_last; + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, EqualityOperatorSame) { + Variant variant(static_cast(3141592654)); + QueryParams params; + IndexedVariant indexed_variant(variant, params); + IndexedVariant identical_indexed_variant(variant, params); + + // Verify the == and != operators return the expected result. + // Check equality with self. + EXPECT_TRUE(indexed_variant == indexed_variant); + EXPECT_FALSE(indexed_variant != indexed_variant); + + // Check equality with identical change. + EXPECT_TRUE(indexed_variant == identical_indexed_variant); + EXPECT_FALSE(indexed_variant != identical_indexed_variant); +} + +TEST(IndexedVariant, EqualityOperatorDifferent) { + Variant variant(static_cast(3141592654)); + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + IndexedVariant indexed_variant(variant, params); + + Variant different_variant(static_cast(2718281828)); + QueryParams different_params; + different_params.order_by = QueryParams::kOrderByChild; + IndexedVariant indexed_variant_different_variant(different_variant, params); + IndexedVariant indexed_variant_different_params(variant, different_params); + IndexedVariant indexed_variant_different_both(different_variant, + different_params); + + // Verify the == and != operators return the expected result. + EXPECT_FALSE(indexed_variant == indexed_variant_different_variant); + EXPECT_TRUE(indexed_variant != indexed_variant_different_variant); + + EXPECT_FALSE(indexed_variant == indexed_variant_different_params); + EXPECT_TRUE(indexed_variant != indexed_variant_different_params); + + EXPECT_FALSE(indexed_variant == indexed_variant_different_both); + EXPECT_TRUE(indexed_variant != indexed_variant_different_both); +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/operation_test.cc b/database/tests/desktop/core/operation_test.cc new file mode 100644 index 0000000000..492f00ba9d --- /dev/null +++ b/database/tests/desktop/core/operation_test.cc @@ -0,0 +1,424 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/operation.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(OperationSource, ConstructorSource) { + OperationSource user_source(OperationSource::kSourceUser); + EXPECT_EQ(user_source.source, OperationSource::kSourceUser); + EXPECT_FALSE(user_source.query_params.has_value()); + EXPECT_FALSE(user_source.tagged); + + OperationSource server_source(OperationSource::kSourceServer); + EXPECT_EQ(server_source.source, OperationSource::kSourceServer); + EXPECT_FALSE(server_source.query_params.has_value()); + EXPECT_FALSE(server_source.tagged); +} + +TEST(OperationSource, ConstructorQueryParams) { + QueryParams params; + OperationSource source((Optional(params))); + + EXPECT_EQ(source.source, OperationSource::kSourceServer); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_FALSE(source.tagged); +} + +TEST(OperationSource, OperationSourceAllArgConstructor) { + QueryParams params; + { + OperationSource source(OperationSource::kSourceServer, + Optional(params), false); + + EXPECT_EQ(source.source, OperationSource::kSourceServer); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_FALSE(source.tagged); + } + { + OperationSource source(OperationSource::kSourceServer, + Optional(params), true); + + EXPECT_EQ(source.source, OperationSource::kSourceServer); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_TRUE(source.tagged); + } + { + OperationSource source(OperationSource::kSourceUser, + Optional(params), false); + + EXPECT_EQ(source.source, OperationSource::kSourceUser); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_FALSE(source.tagged); + } +} + +TEST(OperationSourceDeathTest, BadConstructorArgs) { + QueryParams params; + EXPECT_DEATH(OperationSource(OperationSource::kSourceUser, + Optional(params), true), + ""); +} + +TEST(OperationSource, ForServerTaggedQuery) { + QueryParams params; + OperationSource expected(OperationSource::kSourceServer, + Optional(params), true); + + OperationSource actual = OperationSource::ForServerTaggedQuery(params); + + EXPECT_EQ(actual.source, expected.source); + EXPECT_EQ(actual.query_params, expected.query_params); + EXPECT_EQ(actual.tagged, expected.tagged); +} + +TEST(Operation, Overwrite) { + Operation op = Operation::Overwrite(OperationSource::kServer, Path("A/B/C"), + Variant(100)); + EXPECT_EQ(op.type, Operation::kTypeOverwrite); + EXPECT_EQ(op.source.source, OperationSource::kSourceServer); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); + EXPECT_EQ(op.snapshot, 100); +} + +TEST(Operation, Merge) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + Operation op = + Operation::Merge(OperationSource::kServer, Path("A/B/C"), write); + + EXPECT_EQ(op.type, Operation::kTypeMerge); + EXPECT_EQ(op.source.source, OperationSource::kSourceServer); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); + EXPECT_FALSE(op.children.IsEmpty()); + EXPECT_FALSE(op.children.write_tree().IsEmpty()); + EXPECT_FALSE(op.children.write_tree().value().has_value()); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(op.children.write_tree().GetValueAt(Path("fff")), nullptr); +} + +TEST(Operation, AckUserWrite) { + Tree affected_tree; + affected_tree.SetValueAt(Path("Z/Y/X"), true); + affected_tree.SetValueAt(Path("Z/Y/X/W"), false); + affected_tree.SetValueAt(Path("Z/Y/X/V"), true); + affected_tree.SetValueAt(Path("Z/Y/U"), false); + Operation op = + Operation::AckUserWrite(Path("A/B/C"), affected_tree, kAckRevert); + + EXPECT_EQ(op.type, Operation::kTypeAckUserWrite); + EXPECT_EQ(op.source.source, OperationSource::kSourceUser); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); + EXPECT_TRUE(*op.affected_tree.GetValueAt(Path("Z/Y/X"))); + EXPECT_FALSE(*op.affected_tree.GetValueAt(Path("Z/Y/X/W"))); + EXPECT_TRUE(*op.affected_tree.GetValueAt(Path("Z/Y/X/V"))); + EXPECT_FALSE(*op.affected_tree.GetValueAt(Path("Z/Y/U"))); + EXPECT_TRUE(op.revert); +} + +TEST(Operation, ListenComplete) { + Operation op = + Operation::ListenComplete(OperationSource::kServer, Path("A/B/C")); + EXPECT_EQ(op.type, Operation::kTypeListenComplete); + EXPECT_EQ(op.source.source, OperationSource::kSourceServer); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); +} + +TEST(OperationDeathTest, ListenCompleteWithWrongSource) { + // ListenCompletes must come from the server, not the user. + EXPECT_DEATH(Operation::ListenComplete(OperationSource::kUser, Path("A/B/C")), + DEATHTEST_SIGABRT); +} + +TEST(Operation, OperationForChildOverwriteEmptyPath) { + std::map variant_data{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Operation op = Operation::Overwrite(OperationSource::kServer, Path(), + Variant(variant_data)); + Optional result = OperationForChild(op, "aaa"); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeOverwrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_EQ(result->snapshot, Variant(100)); +} + +TEST(Operation, OperationForChildOverwriteNonEmptyPath) { + std::map variant_data{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Operation op = Operation::Overwrite(OperationSource::kServer, Path("A/B/C"), + Variant(variant_data)); + Optional result = OperationForChild(op, "A"); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeOverwrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); + EXPECT_EQ(result->snapshot, variant_data); +} + +TEST(Operation, OperationForChildMergeEmptyPath) { + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = Operation::Merge(OperationSource::kServer, Path(), write); + + Optional result = OperationForChild(op, "zzz"); + + EXPECT_FALSE(result.has_value()); + } + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = Operation::Merge(OperationSource::kServer, Path(), write); + + Optional result = OperationForChild(op, "aaa"); + + EXPECT_TRUE(result.has_value()); + // In this case we expect to generate an Overwrite operation. + EXPECT_EQ(result->type, Operation::kTypeOverwrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + } + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = Operation::Merge(OperationSource::kServer, Path(), write); + + Optional result = OperationForChild(op, "ccc"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeMerge); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + } +} + +TEST(Operation, OperationForChildMergeNonEmptyPath) { + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = + Operation::Merge(OperationSource::kServer, Path("A/B/C"), write); + + Optional result = OperationForChild(op, "A"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeMerge); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); + const Tree& write_tree = result->children.write_tree(); + EXPECT_EQ(*write_tree.GetValueAt(Path("aaa")), 100); + EXPECT_EQ(*write_tree.GetValueAt(Path("bbb")), 200); + EXPECT_EQ(*write_tree.GetValueAt(Path("ccc/ddd")), 300); + } + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = + Operation::Merge(OperationSource::kServer, Path("A/B/C"), write); + + Optional result = OperationForChild(op, "Z"); + + EXPECT_FALSE(result.has_value()); + } +} + +TEST(Operation, OperationForChildAckUserWriteNonEmptyPath) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = + Operation::AckUserWrite(Path("A/B/C"), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "A"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); + EXPECT_TRUE(*result->affected_tree.GetValueAt(Path("aaa"))); + EXPECT_FALSE(*result->affected_tree.GetValueAt(Path("bbb"))); + EXPECT_TRUE(*result->affected_tree.GetValueAt(Path("ccc/ddd"))); + EXPECT_FALSE(*result->affected_tree.GetValueAt(Path("ccc/eee"))); + EXPECT_TRUE(result->revert); +} + +TEST(OperationDeathTest, + OperationForChildAckUserWriteNonEmptyPathWithUnrelatedChild) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = + Operation::AckUserWrite(Path("A/B/C"), affected_tree, kAckRevert); + + // Cannot ack an unrelated path. + EXPECT_DEATH(OperationForChild(op, "Z"), DEATHTEST_SIGABRT); +} + +TEST(Operation, OperationForChildAckUserWriteEmptyPathHasValue) { + Tree affected_tree; + affected_tree.SetValueAt(Path(), true); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "aaa"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_TRUE(result->affected_tree.value().value()); + EXPECT_TRUE(result->revert); +} + +TEST(OperationDeathTest, + OperationForChildAckUserWriteEmptyPathOverlappingChildren) { + Tree affected_tree; + affected_tree.SetValueAt(Path(), false); + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + // The affected tree has a value at the root which overlaps the affected path. + EXPECT_DEATH(OperationForChild(op, "ccc"), DEATHTEST_SIGABRT); +} + +TEST(Operation, OperationForChildAckUserWriteEmptyPathDoesNotHasValue) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "ccc"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_TRUE(*result->affected_tree.GetValueAt(Path("ddd"))); + EXPECT_FALSE(*result->affected_tree.GetValueAt(Path("eee"))); + EXPECT_TRUE(result->revert); +} + +TEST(Operation, + OperationForChildAckUserWriteEmptyPathDoesNotHasValueAndNoAffectedChild) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "zzz"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_TRUE(result->affected_tree.children().empty()); + EXPECT_FALSE(result->affected_tree.value().has_value()); + EXPECT_TRUE(result->revert); +} + +TEST(Operation, OperationForChildListenCompleteEmptyPath) { + Operation op = Operation::ListenComplete(OperationSource::kServer, Path()); + + Optional result = OperationForChild(op, "Z"); + + // Should be identical to op. + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeListenComplete); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); +} + +TEST(Operation, OperationForChildListenCompleteNonEmptyPath) { + Operation op = + Operation::ListenComplete(OperationSource::kServer, Path("A/B/C")); + + Optional result = OperationForChild(op, "A"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeListenComplete); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/server_values_test.cc b/database/tests/desktop/core/server_values_test.cc new file mode 100644 index 0000000000..6fa2ebb43d --- /dev/null +++ b/database/tests/desktop/core/server_values_test.cc @@ -0,0 +1,221 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/server_values.h" + +#include + +#include "database/src/include/firebase/database/common.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// We expect the result of GenerateServerValues to be pretty close to the +// current time. It might be off by a second or so, but much more than that +// might indicate an issue. +const int kEpsilonMs = 3000; + +TEST(ServerValues, ServerTimestamp) { + EXPECT_EQ(ServerTimestamp(), Variant(std::map{ + std::make_pair(".sv", "timestamp"), + })); +} + +TEST(ServerValues, GenerateServerValues) { + int64_t current_time_ms = time(nullptr) * 1000; + + Variant result = GenerateServerValues(0); + + EXPECT_TRUE(result.is_map()); + EXPECT_EQ(result.map().size(), 1); + EXPECT_NE(result.map().find("timestamp"), result.map().end()); + EXPECT_TRUE(result.map()["timestamp"].is_int64()); + EXPECT_NEAR(result.map()["timestamp"].int64_value(), current_time_ms, + kEpsilonMs); +} + +TEST(ServerValues, GenerateServerValuesWithTimeOffset) { + int64_t current_time_ms = time(nullptr) * 1000; + + Variant result = GenerateServerValues(5000); + + EXPECT_TRUE(result.is_map()); + EXPECT_EQ(result.map().size(), 1); + EXPECT_NE(result.map().find("timestamp"), result.map().end()); + EXPECT_TRUE(result.map()["timestamp"].is_int64()); + EXPECT_NEAR(result.map()["timestamp"].int64_value(), current_time_ms + 5000, + kEpsilonMs); +} + +TEST(ServerValues, ResolveDeferredValueNull) { + Variant null_variant = Variant::Null(); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(null_variant, server_values); + + EXPECT_EQ(result, Variant::Null()); +} + +TEST(ServerValues, ResolveDeferredValueInt64) { + Variant int_variant = Variant::FromInt64(12345); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(int_variant, server_values); + + EXPECT_EQ(result, Variant::FromInt64(12345)); +} + +TEST(ServerValues, ResolveDeferredValueDouble) { + Variant double_variant = Variant::FromDouble(3.14); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(double_variant, server_values); + + EXPECT_EQ(result, Variant::FromDouble(3.14)); +} + +TEST(ServerValues, ResolveDeferredValueBool) { + Variant bool_variant = Variant::FromBool(true); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(bool_variant, server_values); + + EXPECT_EQ(result, Variant::FromBool(true)); +} + +TEST(ServerValues, ResolveDeferredValueStaticString) { + Variant static_string_variant = Variant::FromStaticString("Test"); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(static_string_variant, server_values); + + EXPECT_EQ(result, Variant::FromStaticString("Test")); +} + +TEST(ServerValues, ResolveDeferredValueMutableString) { + Variant mutable_string_variant = Variant::FromMutableString("Test"); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(mutable_string_variant, server_values); + + EXPECT_EQ(result, Variant::FromMutableString("Test")); +} + +TEST(ServerValues, ResolveDeferredValueVector) { + Variant vector_variant = std::vector{1, 2, 3, 4}; + Variant server_values = GenerateServerValues(0); + Variant expected_vector_variant = vector_variant; + + Variant result = ResolveDeferredValueSnapshot(vector_variant, server_values); + + EXPECT_EQ(result, expected_vector_variant); +} + +TEST(ServerValues, ResolveDeferredValueSimpleMap) { + Variant simple_map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant server_values = GenerateServerValues(0); + Variant expected_simple_map_variant = simple_map_variant; + + Variant result = ResolveDeferredValue(simple_map_variant, server_values); + + EXPECT_EQ(result, expected_simple_map_variant); +} + +TEST(ServerValues, ResolveDeferredValueNestedMap) { + Variant nested_map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair("fff", 500), + }), + }; + Variant expected_nested_map_variant = nested_map_variant; + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(nested_map_variant, server_values); + + EXPECT_EQ(result, expected_nested_map_variant); +} + +TEST(ServerValues, ResolveDeferredValueTimestamp) { + int64_t current_time_ms = time(nullptr) * 1000; + Variant timestamp = ServerTimestamp(); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(timestamp, server_values); + + EXPECT_TRUE(result.is_int64()); + EXPECT_NEAR(result.int64_value(), current_time_ms, kEpsilonMs); +} + +TEST(ServerValues, ResolveDeferredValueSnapshot) { + int64_t current_time_ms = time(nullptr) * 1000; + Variant nested_map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair("fff", ServerTimestamp()), + }), + }; + Variant server_values = GenerateServerValues(0); + + Variant result = + ResolveDeferredValueSnapshot(nested_map_variant, server_values); + + EXPECT_EQ(result.map()["aaa"].int64_value(), 100); + EXPECT_EQ(result.map()["bbb"].int64_value(), 200); + EXPECT_EQ(result.map()["ccc"].map()["ddd"].int64_value(), 300); + EXPECT_EQ(result.map()["ccc"].map()["eee"].int64_value(), 400); + EXPECT_NEAR(result.map()["ccc"].map()["fff"].int64_value(), current_time_ms, + kEpsilonMs); +} + +TEST(ServerValues, ResolveDeferredValueMerge) { + int64_t current_time_ms = time(nullptr) * 1000; + Variant merge(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc/ddd", 300), + std::make_pair("ccc/eee", ServerTimestamp()), + }); + CompoundWrite write = CompoundWrite::FromVariantMerge(merge); + Variant server_values = GenerateServerValues(0); + + CompoundWrite result = ResolveDeferredValueMerge(write, server_values); + + EXPECT_EQ(*result.write_tree().GetValueAt(Path("aaa")), 100); + EXPECT_EQ(*result.write_tree().GetValueAt(Path("bbb")), 200); + EXPECT_EQ(*result.write_tree().GetValueAt(Path("ccc/ddd")), 300); + EXPECT_NEAR(result.write_tree().GetValueAt(Path("ccc/eee"))->int64_value(), + current_time_ms, kEpsilonMs); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/sparse_snapshot_tree_test.cc b/database/tests/desktop/core/sparse_snapshot_tree_test.cc new file mode 100644 index 0000000000..4615390d35 --- /dev/null +++ b/database/tests/desktop/core/sparse_snapshot_tree_test.cc @@ -0,0 +1,116 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/sparse_snapshot_tree.h" + +#include "app/src/variant_util.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::StrictMock; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +class Visitor { + public: + virtual ~Visitor() {} + virtual void Visit(const Path& path, const Variant& variant) = 0; +}; + +class MockVisitor : public Visitor { + public: + ~MockVisitor() override {} + MOCK_METHOD(void, Visit, (const Path& path, const Variant& variant), + (override)); +}; + +TEST(SparseSnapshotTreeTest, RememberSimple) { + SparseSnapshotTree tree; + tree.Remember(Path(), 100); + MockVisitor visitor; + + EXPECT_CALL(visitor, Visit(Path(), Variant(100))); + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +TEST(SparseSnapshotTreeTest, RememberTree) { + SparseSnapshotTree tree; + tree.Remember(Path(), std::map{std::make_pair("aaa", 100)}); + tree.Remember(Path("bbb"), 200); + tree.Remember(Path("bbb/ccc"), 300); + tree.Remember(Path("eee"), 400); + MockVisitor visitor; + + EXPECT_CALL(visitor, + Visit(Path(), Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 300), + }), + std::make_pair("eee", 400), + }))); + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +TEST(SparseSnapshotTreeTest, Forget) { + SparseSnapshotTree tree; + tree.Remember(Path(), std::map{std::make_pair("aaa", 100)}); + tree.Remember(Path("bbb"), 200); + tree.Remember(Path("bbb/ccc"), 300); + tree.Remember(Path("eee"), 400); + tree.Forget(Path("aaa")); + tree.Forget(Path("bbb")); + MockVisitor visitor; + + EXPECT_CALL(visitor, Visit(Path("eee"), Variant(400))); + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +TEST(SparseSnapshotTreeTest, Clear) { + SparseSnapshotTree tree; + tree.Remember(Path(), std::map{std::make_pair("aaa", 100)}); + tree.Remember(Path("bbb"), 200); + tree.Remember(Path("bbb/ccc"), 300); + tree.Clear(); + + // Expect no calls to this visitor. + StrictMock visitor; + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/sync_point_test.cc b/database/tests/desktop/core/sync_point_test.cc new file mode 100644 index 0000000000..d4605cf258 --- /dev/null +++ b/database/tests/desktop/core/sync_point_test.cc @@ -0,0 +1,390 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/sync_point.h" + +#include "app/src/include/firebase/variant.h" +#include "app/src/optional.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/cache_policy.h" +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/core/write_tree.h" +#include "database/src/desktop/persistence/persistence_manager.h" +#include "database/src/desktop/persistence/persistence_manager_interface.h" +#include "database/src/include/firebase/database/common.h" +#include "database/src/include/firebase/database/listener.h" +#include "database/tests/desktop/test/matchers.h" +#include "database/tests/desktop/test/mock_cache_policy.h" +#include "database/tests/desktop/test/mock_listener.h" +#include "database/tests/desktop/test/mock_persistence_manager.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_tracked_query_manager.h" + +using ::testing::Eq; +using ::testing::NiceMock; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(SyncPoint, IsEmpty) { + SyncPoint sync_point; + EXPECT_TRUE(sync_point.IsEmpty()); +} + +class SyncPointTest : public ::testing::Test { + public: + SyncPointTest() + : logger_(), + sync_point_(), + persistence_manager_(MakeUnique(), + MakeUnique(), + MakeUnique(), &logger_) {} + + protected: + SystemLogger logger_; + SyncPoint sync_point_; + NiceMock persistence_manager_; +}; + +TEST_F(SyncPointTest, IsNotEmpty) { + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + CacheNode server_cache; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + + sync_point_.AddEventRegistration( + UniquePtr(event_registration), writes_cache_ref, + server_cache, &persistence_manager_); + + EXPECT_FALSE(sync_point_.IsEmpty()); +} + +TEST_F(SyncPointTest, ApplyOperation) { + Operation operation; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + Variant complete_server_cache; + + std::vector results = + sync_point_.ApplyOperation(operation, writes_cache_ref, + &complete_server_cache, &persistence_manager_); + + std::vector expected_results; + + EXPECT_THAT(results, Pointwise(Eq(), expected_results)); +} + +TEST_F(SyncPointTest, AddEventRegistration) { + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + CacheNode server_cache; + // Give the EventRegistrations different QueryParams so that they get placed + // in different Views. + Path path("a/b/c"); + QueryParams value_params; + value_params.end_at_value = 222; + QuerySpec value_spec(path, value_params); + QueryParams child_params; + child_params.start_at_value = 111; + QuerySpec child_spec(path, child_params); + ValueEventRegistration* value_event_registration = + new ValueEventRegistration(nullptr, nullptr, value_spec); + ChildEventRegistration* child_event_registration = + new ChildEventRegistration(nullptr, nullptr, child_spec); + + std::vector value_events = sync_point_.AddEventRegistration( + UniquePtr(value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + std::vector child_events = sync_point_.AddEventRegistration( + UniquePtr(child_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + + std::vector view_results = sync_point_.GetIncompleteQueryViews(); + + std::vector expected_value_events; + std::vector expected_child_events; + + EXPECT_THAT(value_events, Pointwise(Eq(), expected_value_events)); + EXPECT_THAT(child_events, Pointwise(Eq(), expected_child_events)); + + // Local cache gets updated to the values it expects the server to reflect + // eventually. + + CacheNode expected_value_local_cache( + IndexedVariant(Variant::Null(), value_spec.params), false, true); + CacheNode expected_child_local_cache( + IndexedVariant(Variant::Null(), child_spec.params), false, true); + CacheNode expected_server_cache = server_cache; + + EXPECT_EQ(view_results.size(), 2); + EXPECT_EQ(view_results[0]->query_spec(), value_spec); + + EXPECT_EQ(view_results[0]->view_cache(), + ViewCache(expected_value_local_cache, expected_server_cache)); + + EXPECT_THAT(view_results[0]->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {value_event_registration})); + + EXPECT_EQ(view_results[1]->query_spec(), child_spec); + EXPECT_EQ(view_results[1]->view_cache(), + ViewCache(expected_child_local_cache, expected_server_cache)); + EXPECT_THAT(view_results[1]->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {child_event_registration})); +} + +TEST_F(SyncPointTest, RemoveEventRegistration_FromCompleteView) { + Path path("a/b/c"); + // Give the EventRegistrations different QueryParams, but neither one filters, + // so they'll get placed in the same View. + QueryParams query_params; + query_params.order_by = QueryParams::kOrderByChild; + query_params.order_by_child = "Phillip"; + QuerySpec query_spec(path, query_params); + + QueryParams another_query_params; + another_query_params.order_by = QueryParams::kOrderByChild; + another_query_params.order_by_child = "Lillian"; + QuerySpec another_query_spec(path, another_query_params); + + CacheNode server_cache(IndexedVariant(Variant(), query_spec.params), false, + false); + + MockValueListener listener; + MockValueListener another_listener; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + + ValueEventRegistration* value_event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + ValueEventRegistration* another_value_event_registration = + new ValueEventRegistration(nullptr, &another_listener, + another_query_spec); + + // Add some EventRegistrations... + sync_point_.AddEventRegistration( + UniquePtr(value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + sync_point_.AddEventRegistration( + UniquePtr(another_value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + + // ...And then remove one of them. + std::vector removed_specs; + sync_point_.RemoveEventRegistration(another_query_spec, &another_listener, + kErrorNone, &removed_specs); + + // There should be no incomplete views. + std::vector view_results = sync_point_.GetIncompleteQueryViews(); + EXPECT_EQ(view_results.size(), 0); + + // We expect that the local cache will get updated to the values that the + // server will eventually have. + CacheNode expected_local_cache( + IndexedVariant(Variant::Null(), query_spec.params), false, false); + CacheNode expected_server_cache = server_cache; + ViewCache expected_view_cache(expected_local_cache, expected_server_cache); + + // No QuerySpecs were removed, because there is only one Complete QuerySpec. + EXPECT_THAT(removed_specs, Pointwise(Eq(), std::vector{})); + + // Verify that the correct view remains. + const View* view = sync_point_.GetCompleteView(); + EXPECT_EQ(view->query_spec(), query_spec); + EXPECT_EQ(view->view_cache(), expected_view_cache); + EXPECT_THAT(view->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {value_event_registration})); +} + +TEST_F(SyncPointTest, RemoveEventRegistration_FromIncompleteView) { + Path path("a/b/c"); + // Give the EventRegistrations different QueryParams so that they get placed + // in different Views. + QueryParams query_params; + query_params.end_at_value = 222; + QuerySpec query_spec(path, query_params); + + QueryParams another_query_params; + another_query_params.start_at_value = 111; + QuerySpec another_query_spec(path, another_query_params); + + CacheNode server_cache(IndexedVariant(Variant(), query_params), false, false); + + MockValueListener listener; + MockValueListener another_listener; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + + ValueEventRegistration* value_event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + ValueEventRegistration* another_value_event_registration = + new ValueEventRegistration(nullptr, &another_listener, + another_query_spec); + + // Add some EventRegistrations... + sync_point_.AddEventRegistration( + UniquePtr(value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + sync_point_.AddEventRegistration( + UniquePtr(another_value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + + // ...And then remove one of them. + std::vector removed_specs; + sync_point_.RemoveEventRegistration(another_query_spec, &another_listener, + kErrorNone, &removed_specs); + + // There should be one incomplete view remaining. + std::vector view_results = sync_point_.GetIncompleteQueryViews(); + EXPECT_EQ(view_results.size(), 1); + + // We expect that the local cache will get updated to the values that the + // server will eventually have. + CacheNode expected_local_cache(IndexedVariant(Variant::Null(), query_params), + false, true); + CacheNode expected_server_cache = server_cache; + ViewCache expected_view_cache(expected_local_cache, expected_server_cache); + + // Check that the correct QuerySpecs were removed. + EXPECT_THAT(removed_specs, Pointwise(Eq(), {another_query_spec})); + + // Verify that the correct view remain. + const View* view = view_results[0]; + EXPECT_EQ(view->query_spec(), query_spec); + EXPECT_EQ(view->view_cache(), expected_view_cache); + EXPECT_THAT(view->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {value_event_registration})); +} + +TEST_F(SyncPointTest, GetCompleteServerCache) { + Path path; + + EXPECT_EQ(sync_point_.GetCompleteServerCache(path), nullptr); + EXPECT_FALSE(sync_point_.HasCompleteView()); + + // No filtering. + QueryParams apples_query_params; + QuerySpec apples_query_spec(path, apples_query_params); + + // Filtering + QueryParams bananas_query_params; + bananas_query_params.start_at_value = 111; + QuerySpec bananas_query_spec(path, bananas_query_params); + + CacheNode apples_server_cache( + IndexedVariant(Variant("Apples"), apples_query_params), true, false); + CacheNode bananas_server_cache( + IndexedVariant(Variant("Bananas"), bananas_query_params), true, false); + + MockValueListener apples_listener; + MockValueListener bananas_listener; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + + ValueEventRegistration* apples_event_registration = + new ValueEventRegistration(nullptr, &apples_listener, apples_query_spec); + ValueEventRegistration* bananas_event_registration = + new ValueEventRegistration(nullptr, &bananas_listener, + bananas_query_spec); + + sync_point_.AddEventRegistration( + UniquePtr(apples_event_registration), + writes_cache_ref, apples_server_cache, &persistence_manager_); + sync_point_.AddEventRegistration( + UniquePtr(bananas_event_registration), + writes_cache_ref, bananas_server_cache, &persistence_manager_); + + QueryParams carrots_query_params; + carrots_query_params.equal_to_value = "Carrots"; + QuerySpec carrots_query_spec(path, carrots_query_params); + EXPECT_TRUE(sync_point_.ViewExistsForQuery(apples_query_spec)); + EXPECT_TRUE(sync_point_.ViewExistsForQuery(bananas_query_spec)); + EXPECT_FALSE(sync_point_.ViewExistsForQuery(carrots_query_spec)); + + const View* apples_view = sync_point_.ViewForQuery(apples_query_spec); + const View* bananas_view = sync_point_.ViewForQuery(bananas_query_spec); + const View* carrots_view = sync_point_.ViewForQuery(carrots_query_spec); + EXPECT_EQ(apples_view->view_cache().server_snap(), apples_server_cache); + EXPECT_EQ(bananas_view->view_cache().server_snap(), bananas_server_cache); + EXPECT_EQ(carrots_view, nullptr); + + EXPECT_EQ(*sync_point_.GetCompleteServerCache(path), Variant("Apples")); + EXPECT_TRUE(sync_point_.HasCompleteView()); +} + +TEST_F(SyncPointTest, GetCompleteView_FromQuerySpecThatLoadsAllData) { + WriteTree write_tree; + WriteTreeRef write_tree_ref(Path(), &write_tree); + Path path; + + // Values to feed to AddEventRegistration that will result in a "complete" + // View, i.e. a view with no filtering (ordering is okay) + QueryParams good_params; + good_params.order_by = QueryParams::kOrderByChild; + good_params.order_by_child = "Bob"; + QuerySpec good_spec(path, good_params); + CacheNode good_server_cache(IndexedVariant(Variant("good"), good_params), + true, true); + sync_point_.AddEventRegistration( + MakeUnique(nullptr, nullptr, good_spec), + write_tree_ref, good_server_cache, &persistence_manager_); + + // Values to feed to AddEventRegistration that will not result in an + // incomplete View, i.e. a view with some filters on it. This should not be + // returned when we ask for the complete view. + QueryParams bad_params; + bad_params.limit_first = 10; + QuerySpec bad_spec(path, bad_params); + CacheNode incorrect_server_cache(IndexedVariant(Variant("bad"), bad_params), + true, true); + sync_point_.AddEventRegistration( + MakeUnique(nullptr, nullptr, bad_spec), + write_tree_ref, incorrect_server_cache, &persistence_manager_); + + const View* result = sync_point_.GetCompleteView(); + EXPECT_NE(result, nullptr); + EXPECT_EQ(result->query_spec(), good_spec); + EXPECT_EQ(result->GetLocalCache(), "good"); +} + +TEST_F(SyncPointTest, GetCompleteView_FromQuerySpecThatDoesNotLoadsAllData) { + WriteTree write_tree; + WriteTreeRef write_tree_ref(Path(), &write_tree); + Path path; + + // Values to feed to AddEventRegistration that will not result in an + // incomplete View, i.e. a view with some filters on it. This should not be + // retuened when we ask for the complete view. + QueryParams bad_params; + bad_params.limit_first = 10; + QuerySpec bad_spec(path, bad_params); + CacheNode incorrect_server_cache(IndexedVariant(Variant("bad"), bad_params), + true, true); + sync_point_.AddEventRegistration( + MakeUnique(nullptr, nullptr, bad_spec), + write_tree_ref, incorrect_server_cache, &persistence_manager_); + + EXPECT_EQ(sync_point_.GetCompleteView(), nullptr); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/sync_tree_test.cc b/database/tests/desktop/core/sync_tree_test.cc new file mode 100644 index 0000000000..fad14e5a63 --- /dev/null +++ b/database/tests/desktop/core/sync_tree_test.cc @@ -0,0 +1,825 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/sync_tree.h" + +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/desktop/persistence/persistence_manager.h" +#include "database/src/desktop/persistence/persistence_manager_interface.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" +#include "database/src/include/firebase/database/common.h" +#include "database/tests/desktop/test/mock_cache_policy.h" +#include "database/tests/desktop/test/mock_listen_provider.h" +#include "database/tests/desktop/test/mock_listener.h" +#include "database/tests/desktop/test/mock_persistence_manager.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_tracked_query_manager.h" +#include "database/tests/desktop/test/mock_write_tree.h" + +using ::testing::NiceMock; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::Test; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(SyncTree, Constructor) { + UniquePtr write_tree; + UniquePtr persistence_manager; + UniquePtr listen_provider; + SyncTree sync_tree(std::move(write_tree), std::move(persistence_manager), + std::move(listen_provider)); + + // Just making sure this constructor doesn't crash or leak memory. No further + // tests. +} + +class SyncTreeTest : public Test { + public: + void SetUp() override { + // These mocks are very noisy, so we make them NiceMocks and explicitly call + // EXPECT_CALL when there are specific things we expect to have happen. + UniquePtr pending_write_tree_ptr(MakeUnique()); + + persistence_storage_engine_ = new NiceMock(); + UniquePtr storage_engine_ptr( + persistence_storage_engine_); + + tracked_query_manager_ = new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager_); + + cache_policy_ = new NiceMock(); + UniquePtr cache_policy_ptr(cache_policy_); + + persistence_manager_ = new NiceMock( + std::move(storage_engine_ptr), std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger_); + UniquePtr persistence_manager_ptr( + persistence_manager_); + + listen_provider_ = new NiceMock(); + UniquePtr listen_provider_ptr(listen_provider_); + + sync_tree_ = new SyncTree(std::move(pending_write_tree_ptr), + std::move(persistence_manager_ptr), + std::move(listen_provider_ptr)); + } + + void TearDown() override { delete sync_tree_; } + + protected: + // We keep a local copy of these pointers so that we can do expectation + // testing on them. The SyncTree (or the classes SyncTree owns) own these + // pointers though so we let them handle the cleanup. + MockWriteTree* pending_write_tree_; + MockPersistenceStorageEngine* persistence_storage_engine_; + MockTrackedQueryManager* tracked_query_manager_; + MockCachePolicy* cache_policy_; + SystemLogger logger_; + MockPersistenceManager* persistence_manager_; + MockListenProvider* listen_provider_; + + SyncTree* sync_tree_; +}; + +using SyncTreeDeathTest = SyncTreeTest; + +TEST_F(SyncTreeTest, AddEventRegistration) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + EXPECT_TRUE(sync_tree_->IsEmpty()); + EXPECT_CALL(*persistence_manager_, SetQueryActive(query_spec)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + EXPECT_FALSE(sync_tree_->IsEmpty()); +} + +TEST_F(SyncTreeTest, ApplyListenComplete) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + CacheNode initial_cache(IndexedVariant(Variant(), query_spec.params), true, + false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Applying a ListenComplete should tell the PersistenceManager that listening + // on the given query is complete. + EXPECT_CALL(*persistence_manager_, SetQueryComplete(query_spec)); + std::vector results = sync_tree_->ApplyListenComplete(path); + EXPECT_EQ(results, std::vector{}); +} + +TEST_F(SyncTreeTest, ApplyServerMerge) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + std::map changed_children{ + std::make_pair(Path("fruit/apple"), "green"), + std::make_pair(Path("fruit/banana"), "yellow"), + }; + + // Apply the merge and get the results. + std::vector results = + sync_tree_->ApplyServerMerge(path, changed_children); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, ApplyServerOverwrite) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + std::map changed_children{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }; + + // Apply the override and get the results. + std::vector results = + sync_tree_->ApplyServerOverwrite(path, changed_children); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, ApplyUserMerge) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + CompoundWrite unresolved_children = + CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("fruit/apple"), "green"), + std::make_pair(Path("fruit/banana"), "yellow"), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + CompoundWrite children = unresolved_children; + WriteId write_id = 100; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserMerge(path, unresolved_children, write_id)); + + // Apply the user merge and get the results. + std::vector results = sync_tree_->ApplyUserMerge( + path, unresolved_children, children, write_id, kPersist); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, ApplyUserOverwrite) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + // Apply the user merge and get the results. + std::vector results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, AckUserWrite) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + std::vector results; + std::vector expected_results; + // Apply the user merge and get the results. + results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); + + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + results = sync_tree_->AckUserWrite(write_id, kAckConfirm, kPersist, 0); + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, AckUserWriteRevert) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + std::vector results; + std::vector expected_results; + // Apply the user merge and get the results. + results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); + + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + results = sync_tree_->AckUserWrite(write_id, kAckRevert, kPersist, 0); + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, RemoveAllWrites) { + // This starts off the same as the ApplyUserOverwrite test, but then + // afterward. + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the + // PersistenceManager, but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + // Apply the user merge and get the results. + std::vector results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); + + // We now have a pending write to undo. Verify we get the right events. + EXPECT_CALL(*persistence_manager_, RemoveAllUserWrites()); + std::vector remove_results = sync_tree_->RemoveAllWrites(); + std::vector expected_remove_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(remove_results, expected_remove_results); +} + +TEST_F(SyncTreeTest, RemoveAllEventRegistrations) { + QueryParams loads_all_data; + QueryParams does_not_load_all_data; + does_not_load_all_data.limit_first = 10; + QuerySpec query_spec1(Path("aaa/bbb/ccc"), loads_all_data); + // Two QuerySpecs at same location but different parameters. + QuerySpec query_spec2(Path("aaa/bbb/ccc"), does_not_load_all_data); + // Shadowing QuerySpec at higher location . + QuerySpec query_spec3(Path("aaa"), loads_all_data); + // QuerySpec in a totally different area of the tree. + QuerySpec query_spec4(Path("ddd/eee/fff"), does_not_load_all_data); + MockValueListener listener1; + MockChildListener listener2; + MockValueListener listener3; + MockChildListener listener4; + ValueEventRegistration* event_registration1 = + new ValueEventRegistration(nullptr, &listener1, query_spec1); + ChildEventRegistration* event_registration2 = + new ChildEventRegistration(nullptr, &listener2, query_spec2); + ValueEventRegistration* event_registration3 = + new ValueEventRegistration(nullptr, &listener3, query_spec3); + ChildEventRegistration* event_registration4 = + new ChildEventRegistration(nullptr, &listener4, query_spec4); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration1)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration2)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration3)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration4)); + + std::vector results; + // This will not cause any calls to StopListening because the listener + // is listening on aaa and redirecting changes to this location internally. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec1)).Times(2); + results = sync_tree_->RemoveAllEventRegistrations(query_spec1, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // This will cause the ListenProvider to stop listening on aaa because it is + // the rootmost listener on this location. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec3)); + EXPECT_CALL(*listen_provider_, StopListening(query_spec3, Tag())); + results = sync_tree_->RemoveAllEventRegistrations(query_spec3, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // In the case of an error, no explicit call to StopListening is made. This + // is expected. However, we will stop tracking the query. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec4)); + results = + sync_tree_->RemoveAllEventRegistrations(query_spec4, kErrorExpiredToken); + + // I have to manually construct this because normally building an 'error' + // event requres that I pass in a UniquePtr. + Event expected_event; + expected_event.type = kEventTypeError; + expected_event.event_registration = event_registration4; + expected_event.snapshot = Optional(); + expected_event.error = kErrorExpiredToken; + expected_event.path = Path("ddd/eee/fff"); + EXPECT_EQ(results, std::vector{expected_event}); +} + +TEST_F(SyncTreeTest, RemoveEventRegistration) { + QueryParams loads_all_data; + QueryParams does_not_load_all_data; + does_not_load_all_data.limit_first = 10; + QuerySpec query_spec1(Path("aaa/bbb/ccc"), loads_all_data); + // Two QuerySpecs at same location but different parameters. + QuerySpec query_spec2(Path("aaa/bbb/ccc"), does_not_load_all_data); + // Shadowing QuerySpec at higher location . + QuerySpec query_spec3(Path("aaa"), loads_all_data); + // QuerySpec in a totally different area of the tree. + QuerySpec query_spec4(Path("ddd/eee/fff"), does_not_load_all_data); + MockValueListener listener1; + MockChildListener listener2; + MockValueListener listener3; + MockChildListener listener4; + MockValueListener unassigned_listener; + ValueEventRegistration* event_registration1 = + new ValueEventRegistration(nullptr, &listener1, query_spec1); + ChildEventRegistration* event_registration2 = + new ChildEventRegistration(nullptr, &listener2, query_spec2); + ValueEventRegistration* event_registration3 = + new ValueEventRegistration(nullptr, &listener3, query_spec3); + ChildEventRegistration* event_registration4 = + new ChildEventRegistration(nullptr, &listener4, query_spec4); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration1)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration2)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration3)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration4)); + + std::vector results; + // This will not cause any calls to StopListening because the listener + // is listening on aaa and redirecting changes to this location internally. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec1)).Times(2); + results = + sync_tree_->RemoveEventRegistration(query_spec1, &listener1, kErrorNone); + EXPECT_EQ(results, std::vector{}); + results = + sync_tree_->RemoveEventRegistration(query_spec1, &listener2, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // Expect nothing to happen. + results = sync_tree_->RemoveEventRegistration( + query_spec1, &unassigned_listener, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // This will cause the ListenProvider to stop listening on aaa because it is + // the rootmost listener on this location. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec3)); + EXPECT_CALL(*listen_provider_, StopListening(query_spec3, Tag())); + results = + sync_tree_->RemoveEventRegistration(query_spec3, &listener3, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // In the case of an error, no explicit call to StopListening is made. This + // is expected. However, we will stop tracking the query. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec4)); + results = sync_tree_->RemoveEventRegistration(query_spec4, nullptr, + kErrorExpiredToken); + + // I have to manually construct this because normally constructing an 'error' + // event requres that I pass in a UniquePtr. + Event expected_event; + expected_event.type = kEventTypeError; + expected_event.event_registration = event_registration4; + expected_event.snapshot = Optional(); + expected_event.error = kErrorExpiredToken; + expected_event.path = Path("ddd/eee/fff"); + EXPECT_EQ(results, std::vector{expected_event}); +} + +TEST_F(SyncTreeDeathTest, RemoveEventRegistration) { + QuerySpec query_spec(Path("i/am/become/death")); + MockChildListener listener; + ChildEventRegistration* event_registration = + new ChildEventRegistration(nullptr, &listener, query_spec); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + EXPECT_DEATH(sync_tree_->RemoveEventRegistration(query_spec, &listener, + kErrorExpiredToken), + DEATHTEST_SIGABRT); +} + +TEST(SyncTree, CalcCompleteEventCache) { + // For this test we set up our own sync tree instead of using the premade test + // harness because we need a mock write tree instead of a functional one to + // run this test. + // + // TODO(amablue): retrofit the other tests to function with a MockWriteTree by + // filling in the expected values to calls to the write tree. + SystemLogger logger; + MockWriteTree* pending_write_tree = new NiceMock(); + UniquePtr pending_write_tree_ptr(pending_write_tree); + MockPersistenceManager* persistence_manager = + new NiceMock( + MakeUnique>(), + MakeUnique>(), + MakeUnique>(), &logger); + UniquePtr persistence_manager_ptr( + persistence_manager); + SyncTree sync_tree(std::move(pending_write_tree_ptr), + std::move(persistence_manager_ptr), + MakeUnique>()); + + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + + sync_tree.AddEventRegistration( + UniquePtr(event_registration)); + + std::vector write_ids_to_exclude{1, 2, 3, 4}; + Variant expected_server_cache(std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }); + EXPECT_CALL(*pending_write_tree, + CalcCompleteEventCache( + Path("aaa/bbb/ccc/fruit"), Pointee(expected_server_cache), + write_ids_to_exclude, kIncludeHiddenWrites)); + sync_tree.CalcCompleteEventCache(Path("aaa/bbb/ccc/fruit"), + write_ids_to_exclude); +} + +TEST_F(SyncTreeTest, SetKeepSynchronized) { + QuerySpec query_spec1(Path("aaa/bbb/ccc")); + QuerySpec query_spec2(Path("aaa/bbb/ccc/ddd")); + + EXPECT_CALL(*persistence_manager_, SetQueryActive(query_spec1)); + sync_tree_->SetKeepSynchronized(query_spec1, true); + + EXPECT_CALL(*persistence_manager_, SetQueryActive(query_spec2)); + sync_tree_->SetKeepSynchronized(query_spec2, true); + + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec1)); + sync_tree_->SetKeepSynchronized(query_spec1, false); + + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec2)); + sync_tree_->SetKeepSynchronized(query_spec2, false); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/tracked_query_manager_test.cc b/database/tests/desktop/core/tracked_query_manager_test.cc new file mode 100644 index 0000000000..1723301865 --- /dev/null +++ b/database/tests/desktop/core/tracked_query_manager_test.cc @@ -0,0 +1,396 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/tracked_query_manager.h" + +#include "app/src/logger.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" + +using testing::_; +using testing::InSequence; +using testing::NiceMock; +using testing::Return; +using testing::UnorderedElementsAre; + +namespace firebase { +namespace database { +namespace internal { + +TEST(TrackedQuery, Equality) { + TrackedQuery query(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, TrackedQuery::kInactive); + TrackedQuery same(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, TrackedQuery::kInactive); + TrackedQuery different_query_id(999, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + TrackedQuery different_query_spec(123, QuerySpec(Path("some/other/path")), + 123, TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + TrackedQuery different_complete(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kComplete, + TrackedQuery::kInactive); + TrackedQuery different_active(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, + TrackedQuery::kActive); + + // Check for equality. + EXPECT_TRUE(query == same); + EXPECT_FALSE(query != same); + + // Check each way it can differ. + EXPECT_FALSE(query == different_query_id); + EXPECT_TRUE(query != different_query_id); + + EXPECT_FALSE(query == different_query_spec); + EXPECT_TRUE(query != different_query_spec); + + EXPECT_FALSE(query == different_complete); + EXPECT_TRUE(query != different_complete); + + EXPECT_FALSE(query == different_active); + EXPECT_TRUE(query != different_active); +} + +TEST(TrackedQueryManager, Constructor) { + MockPersistenceStorageEngine storage_engine; + SystemLogger logger; + + InSequence seq; + EXPECT_CALL(storage_engine, BeginTransaction()); + EXPECT_CALL(storage_engine, ResetPreviouslyActiveTrackedQueries(_)); + EXPECT_CALL(storage_engine, SetTransactionSuccessful()); + EXPECT_CALL(storage_engine, EndTransaction()); + EXPECT_CALL(storage_engine, LoadTrackedQueries()); + TrackedQueryManager manager(&storage_engine, &logger); +} + +class TrackedQueryManagerTest : public ::testing::Test { + void SetUp() override { + spec_incomplete_inactive_.path = Path("test/path/incomplete_inactive"); + spec_incomplete_active_.path = Path("test/path/incomplete_active"); + spec_complete_inactive_.path = Path("test/path/complete_inactive"); + spec_complete_active_.path = Path("test/path/complete_active"); + + // Populate with fake data. + ON_CALL(storage_engine_, LoadTrackedQueries()) + .WillByDefault(Return(std::vector{ + TrackedQuery(100, spec_incomplete_inactive_, 0, + TrackedQuery::kIncomplete, TrackedQuery::kInactive), + TrackedQuery(200, spec_incomplete_active_, 0, + TrackedQuery::kIncomplete, TrackedQuery::kActive), + TrackedQuery(300, spec_complete_inactive_, 0, + TrackedQuery::kComplete, TrackedQuery::kInactive), + TrackedQuery(400, spec_complete_active_, 0, TrackedQuery::kComplete, + TrackedQuery::kActive), + })); + + manager_ = new TrackedQueryManager(&storage_engine_, &logger_); + } + + void TearDown() override { delete manager_; } + + protected: + SystemLogger logger_; + NiceMock storage_engine_; + TrackedQueryManager* manager_; + + QuerySpec spec_incomplete_inactive_; + QuerySpec spec_incomplete_active_; + QuerySpec spec_complete_inactive_; + QuerySpec spec_complete_active_; +}; + +// We need the death tests to be separate from the regular tests, but we still +// want to set up the same data. +class TrackedQueryManagerDeathTest : public TrackedQueryManagerTest {}; + +TEST_F(TrackedQueryManagerTest, FindTrackedQuery_Success) { + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_FALSE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(result->query_id, 200); + EXPECT_EQ(result->query_spec, spec_incomplete_active_); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(result->query_id, 300); + EXPECT_EQ(result->query_spec, spec_complete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_active_); + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, FindTrackedQuery_Failure) { + QuerySpec bad_spec(Path("wrong/path")); + const TrackedQuery* result = manager_->FindTrackedQuery(bad_spec); + EXPECT_EQ(result, nullptr); +} + +TEST_F(TrackedQueryManagerTest, RemoveTrackedQuery) { + EXPECT_NE(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(100)); + manager_->RemoveTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(200)); + manager_->RemoveTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(300)); + manager_->RemoveTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(400)); + manager_->RemoveTrackedQuery(spec_complete_active_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_complete_active_), nullptr); +} + +TEST_F(TrackedQueryManagerDeathTest, RemoveTrackedQuery_Failure) { + QuerySpec not_tracked(Path("a/path/not/being/tracked")); + // Can't remove a query unless you're already tracking it. + EXPECT_DEATH(manager_->RemoveTrackedQuery(not_tracked), DEATHTEST_SIGABRT); +} + +TEST_F(TrackedQueryManagerTest, SetQueryActiveFlag_NewQuery) { + QuerySpec new_spec(Path("new/active/query")); + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(new_spec, TrackedQuery::kActive); + const TrackedQuery* result = manager_->FindTrackedQuery(new_spec); + + // result->query_id should be one digit higher than the highest query_id + // loaded. + EXPECT_EQ(result->query_id, 401); + EXPECT_EQ(result->query_spec.params, new_spec.params); + EXPECT_EQ(result->query_spec.path, new_spec.path); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryActiveFlag_ExistingQueryAlreadyTrue) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(spec_complete_active_, TrackedQuery::kActive); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_complete_active_); + + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryActiveFlag_ExistingQueryWasFalse) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(spec_incomplete_inactive_, + TrackedQuery::kActive); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerDeathTest, SetQueryInactive_NewQuery) { + QuerySpec new_spec(Path("new/active/query")); + // Can't set a query inactive unless you are already tracking it. + EXPECT_DEATH(manager_->SetQueryActiveFlag(new_spec, TrackedQuery::kInactive), + DEATHTEST_SIGABRT); +} + +TEST_F(TrackedQueryManagerTest, SetQueryInactive_ExistingQuery) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(spec_complete_active_, TrackedQuery::kInactive); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_complete_active_); + + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryCompleteIfExists_DoesExist) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryCompleteIfExists(spec_incomplete_inactive_); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryCompleteIfExists_DoesNotExist) { + QuerySpec new_spec(Path("new/active/query")); + manager_->SetQueryCompleteIfExists(new_spec); + const TrackedQuery* result = manager_->FindTrackedQuery(new_spec); + + EXPECT_EQ(result, nullptr); +} + +TEST_F(TrackedQueryManagerTest, SetQueriesComplete_CorrectPath) { + // Only two of our four TrackedQueries will need to be updated, and thus saved + // in the database. + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)).Times(2); + manager_->SetQueriesComplete(Path("test/path")); + + // All Tracked Queries should be complete. + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(result->query_id, 200); + EXPECT_EQ(result->query_spec, spec_incomplete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(result->query_id, 300); + EXPECT_EQ(result->query_spec, spec_complete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_active_); + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueriesComplete_IncorrectPath) { + manager_->SetQueriesComplete(Path("wrong/test/path")); + + // All Tracked Queries should be unchanged. + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_FALSE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(result->query_id, 200); + EXPECT_EQ(result->query_spec, spec_incomplete_active_); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(result->query_id, 300); + EXPECT_EQ(result->query_spec, spec_complete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_active_); + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, IsQueryComplete) { + EXPECT_FALSE(manager_->IsQueryComplete(spec_incomplete_inactive_)); + EXPECT_FALSE(manager_->IsQueryComplete(spec_incomplete_active_)); + EXPECT_TRUE(manager_->IsQueryComplete(spec_complete_inactive_)); + EXPECT_TRUE(manager_->IsQueryComplete(spec_complete_active_)); + + EXPECT_FALSE(manager_->IsQueryComplete(QuerySpec(Path("nonexistant")))); +} + +TEST_F(TrackedQueryManagerTest, GetKnownCompleteChildren) { + EXPECT_THAT(manager_->GetKnownCompleteChildren(Path("test/path")), + UnorderedElementsAre("complete_inactive", "complete_active")); +} + +TEST_F(TrackedQueryManagerTest, + EnsureCompleteTrackedQuery_ExistingUncompletedQuery) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->EnsureCompleteTrackedQuery(Path("test/path/incomplete_inactive")); + + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, EnsureCompleteTrackedQuery_NewPath) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + Path new_path("new/path"); + manager_->EnsureCompleteTrackedQuery(new_path); + + const TrackedQuery* result = manager_->FindTrackedQuery(QuerySpec(new_path)); + EXPECT_EQ(result->query_id, 401); + EXPECT_EQ(result->query_spec, QuerySpec(new_path)); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, HasActiveDefaultQuery) { + EXPECT_FALSE( + manager_->HasActiveDefaultQuery(Path("test/path/incomplete_inactive"))); + EXPECT_TRUE( + manager_->HasActiveDefaultQuery(Path("test/path/incomplete_active"))); + EXPECT_FALSE( + manager_->HasActiveDefaultQuery(Path("test/path/complete_inactive"))); + EXPECT_TRUE( + manager_->HasActiveDefaultQuery(Path("test/path/complete_active"))); + + EXPECT_FALSE(manager_->IsQueryComplete(QuerySpec(Path("nonexistant")))); +} + +TEST_F(TrackedQueryManagerTest, CountOfPrunableQueries) { + EXPECT_EQ(manager_->CountOfPrunableQueries(), 2); +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/tree_test.cc b/database/tests/desktop/core/tree_test.cc new file mode 100644 index 0000000000..34b3e92a20 --- /dev/null +++ b/database/tests/desktop/core/tree_test.cc @@ -0,0 +1,1009 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/tree.h" + +#include "app/memory/unique_ptr.h" +#include "app/src/optional.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +namespace { + +using ::testing::Eq; + +typedef std::pair IntPair; + +TEST(TreeTest, DefaultConstruct) { + { + Tree tree; + EXPECT_FALSE(tree.value().has_value()); + EXPECT_EQ(tree.children().size(), 0); + } + + { + Tree tree(1); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 0); + } +} + +TEST(TreeTest, CopyConstructor) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(source); + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure source is still populated. + subtree = source.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*source.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); +} + +TEST(TreeTest, CopyAssignment) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(-9999); + destination.SetValueAt(Path("zzz/yyy/xxx"), -9999); + + destination = source; + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure old values were not left behind. + Tree* bad_subtree = destination.GetChild(Path("zzz/yyy/xxx")); + EXPECT_EQ(bad_subtree, nullptr); + + // Ensure source is still populated. + subtree = source.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*source.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); +} + +TEST(TreeTest, MoveConstructor) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(std::move(source)); + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure source is empty. + EXPECT_FALSE(source.value().has_value()); // NOLINT + EXPECT_TRUE(source.children().empty()); // NOLINT +} + +TEST(TreeTest, MoveAssignment) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(-9999); + destination.SetValueAt(Path("zzz/yyy/xxx"), -9999); + + destination = std::move(source); + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure old values were not left behind. + Tree* bad_subtree = destination.GetChild(Path("zzz/yyy/xxx")); + EXPECT_EQ(bad_subtree, nullptr); + + // Ensure source is empty. + EXPECT_FALSE(source.value().has_value()); // NOLINT + EXPECT_TRUE(source.children().empty()); // NOLINT +} + +TEST(TreeTest, GetSetValue) { + { + Tree tree(1); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + + tree.set_value(2); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 2); + } +} + +TEST(TreeTest, GetSetRValue) { + { + Tree> tree(MakeUnique(1)); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(*tree.value().value(), 1); + + tree.set_value(MakeUnique(2)); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(*tree.value().value(), 2); + } +} + +TEST(TreeTest, GetValueAt) { + { + Tree tree; + + int* root = tree.GetValueAt(Path("")); + EXPECT_EQ(root, nullptr); + EXPECT_EQ(tree.GetValueAt(Path("A")), nullptr); + } + + { + Tree tree(1); + + int* root = tree.GetValueAt(Path("")); + EXPECT_NE(root, nullptr); + EXPECT_EQ(*root, 1); + EXPECT_EQ(tree.GetValueAt(Path("A")), nullptr); + } + + { + Tree tree(1); + tree.children()["A"].set_value(2); + tree.children()["B"].set_value(3); + + int* root = tree.GetValueAt(Path("")); + EXPECT_NE(root, nullptr); + EXPECT_EQ(*root, 1); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_NE(child_b, nullptr); + EXPECT_EQ(*child_b, 3); + } + + { + Tree tree(1); + tree.children()["A"].set_value(2); + tree.children()["A"].children()["A1"].set_value(20); + tree.children()["B"].children()["B1"].set_value(30); + + int* root = tree.GetValueAt(Path("")); + EXPECT_NE(root, nullptr); + EXPECT_EQ(*root, 1); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_a_a1 = tree.GetValueAt(Path("A/A1")); + EXPECT_NE(child_a_a1, nullptr); + EXPECT_EQ(*child_a_a1, 20); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_EQ(child_b, nullptr); + + int* child_b_b1 = tree.GetValueAt(Path("B/B1")); + EXPECT_NE(child_b_b1, nullptr); + EXPECT_EQ(*child_b_b1, 30); + } +} + +TEST(TreeTest, SetValueAt) { + { + Tree tree; + tree.SetValueAt(Path(""), 1); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 0); + } + + { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("B"), 3); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_NE(child_b, nullptr); + EXPECT_EQ(*child_b, 3); + } + + { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("A/A1"), 20); + tree.SetValueAt(Path("B/B1"), 30); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_a_a1 = tree.GetValueAt(Path("A/A1")); + EXPECT_NE(child_a_a1, nullptr); + EXPECT_EQ(*child_a_a1, 20); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_EQ(child_b, nullptr); + + int* child_b_b1 = tree.GetValueAt(Path("B/B1")); + EXPECT_NE(child_b_b1, nullptr); + EXPECT_EQ(*child_b_b1, 30); + } +} + +TEST(TreeTest, SetValueAtRValue) { + { + Tree> tree; + tree.SetValueAt(Path(""), MakeUnique(1)); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 0); + } + + { + Tree> tree(MakeUnique(1)); + tree.SetValueAt(Path("A"), MakeUnique(2)); + tree.SetValueAt(Path("B"), MakeUnique(3)); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + UniquePtr* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(**child_a, 2); + + UniquePtr* child_b = tree.GetValueAt(Path("B")); + EXPECT_NE(child_b, nullptr); + EXPECT_EQ(**child_b, 3); + } + + { + Tree> tree(MakeUnique(1)); + tree.SetValueAt(Path("A"), MakeUnique(2)); + tree.SetValueAt(Path("A/A1"), MakeUnique(20)); + tree.SetValueAt(Path("B/B1"), MakeUnique(30)); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + UniquePtr* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(**child_a, 2); + + UniquePtr* child_a_a1 = tree.GetValueAt(Path("A/A1")); + EXPECT_NE(child_a_a1, nullptr); + EXPECT_EQ(**child_a_a1, 20); + + UniquePtr* child_b = tree.GetValueAt(Path("B")); + EXPECT_EQ(child_b, nullptr); + + UniquePtr* child_b_b1 = tree.GetValueAt(Path("B/B1")); + EXPECT_NE(child_b_b1, nullptr); + EXPECT_EQ(**child_b_b1, 30); + } +} + +TEST(TreeTest, RootMostValue) { + { + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {3, 4}); + tree.SetValueAt(Path("A/B"), {5, 6}); + tree.SetValueAt(Path("A/B/C"), {7, 8}); + tree.SetValueAt(Path("A/B/D"), {9, 10}); + tree.SetValueAt(Path("A/B/D"), {1, 9999}); + EXPECT_EQ(*tree.RootMostValue(Path()), std::make_pair(1, 2)); + EXPECT_EQ(*tree.RootMostValue(Path("A")), std::make_pair(1, 2)); + EXPECT_EQ(*tree.RootMostValue(Path("B")), std::make_pair(1, 2)); + } + { + Tree tree; + tree.SetValueAt(Path("A/B"), {5, 6}); + tree.SetValueAt(Path("Z/Z"), {5, -9999}); + tree.SetValueAt(Path("A/B/C"), {7, 8}); + tree.SetValueAt(Path("A/B/D"), {9, 10}); + EXPECT_EQ(tree.RootMostValue(Path()), nullptr); + EXPECT_EQ(tree.RootMostValue(Path("A")), nullptr); + EXPECT_EQ(tree.RootMostValue(Path("B")), nullptr); + EXPECT_EQ(*tree.RootMostValue(Path("A/B")), std::make_pair(5, 6)); + EXPECT_EQ(*tree.RootMostValue(Path("A/B/C")), std::make_pair(5, 6)); + } + { + Tree tree; + EXPECT_EQ(tree.RootMostValue(Path()), nullptr); + } +} + +TEST(TreeTest, RootMostValueMatching) { + auto find_three = [](const IntPair& value) { return value.first == 3; }; + { + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {3, 4}); + tree.SetValueAt(Path("A/B"), {5, 6}); + tree.SetValueAt(Path("A/B/C"), {3, -9999}); + tree.SetValueAt(Path("A/B/D"), {9, 10}); + EXPECT_EQ(tree.RootMostValueMatching(Path(), find_three), nullptr); + EXPECT_EQ(*tree.RootMostValueMatching(Path("A"), find_three), + std::make_pair(3, 4)); + EXPECT_EQ(*tree.RootMostValueMatching(Path("A/B/C"), find_three), + std::make_pair(3, 4)); + EXPECT_EQ(tree.RootMostValueMatching(Path("B"), find_three), nullptr); + } + { + Tree tree; + EXPECT_EQ(tree.RootMostValueMatching(Path(), find_three), nullptr); + } +} + +TEST(TreeTest, LeafMostValue) { + { + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {1, 3}); + tree.SetValueAt(Path("A/B"), {1, 4}); + tree.SetValueAt(Path("A/B/C"), {1, 5}); + tree.SetValueAt(Path("A/B/D"), {1, 6}); + EXPECT_EQ(*tree.LeafMostValue(Path()), std::make_pair(1, 2)); + EXPECT_EQ(*tree.LeafMostValue(Path("A")), std::make_pair(1, 3)); + EXPECT_EQ(*tree.LeafMostValue(Path("A/B")), std::make_pair(1, 4)); + EXPECT_EQ(*tree.LeafMostValue(Path("A/B/C")), std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValue(Path("A/B/C/D")), std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValue(Path("B")), std::make_pair(1, 2)); + } + { + Tree tree; + EXPECT_EQ(tree.LeafMostValue(Path()), nullptr); + } +} + +TEST(TreeTest, LeafMostValueMatching) { + { + auto find_one = [](const IntPair& value) { return value.first == 1; }; + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {1, 3}); + tree.SetValueAt(Path("A/B"), {1, 4}); + tree.SetValueAt(Path("A/B/C"), {1, 5}); + tree.SetValueAt(Path("A/B/D"), {1, 6}); + EXPECT_EQ(*tree.LeafMostValueMatching(Path(), find_one), + std::make_pair(1, 2)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A"), find_one), + std::make_pair(1, 3)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A/B"), find_one), + std::make_pair(1, 4)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A/B/C"), find_one), + std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A/B/C/D"), find_one), + std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("B"), find_one), + std::make_pair(1, 2)); + } + { + Tree tree; + EXPECT_EQ(tree.LeafMostValue(Path()), nullptr); + } +} + +TEST(TreeTest, ContainsMatchingValue) { + { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("A/B"), 3); + tree.SetValueAt(Path("A/B/C"), 4); + tree.SetValueAt(Path("A/B/D"), 5); + + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 1; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 2; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 3; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 4; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 5; })); + EXPECT_FALSE( + tree.ContainsMatchingValue([](int value) { return value == 6; })); + } + { + Tree tree; + EXPECT_FALSE( + tree.ContainsMatchingValue([](int value) { return value == 0; })); + } +} + +TEST(TreeTest, GetChild) { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("B/B1"), 30); + + Tree* root_string = tree.GetChild(""); + const Tree* root_const_string = tree.GetChild(""); + Tree* root_path = tree.GetChild(Path("")); + const Tree* root_const_path = tree.GetChild(Path("")); + EXPECT_EQ(root_string, &tree); + EXPECT_EQ(root_const_string, &tree); + EXPECT_EQ(root_path, &tree); + EXPECT_EQ(root_const_path, &tree); + + // Test A + Tree* expected_child_a = &tree.children()["A"]; + Tree* child_a_string = tree.GetChild("A"); + const Tree* child_a_const_string = tree.GetChild("A"); + Tree* child_a_path = tree.GetChild(Path("A")); + const Tree* child_a_const_path = tree.GetChild(Path("A")); + EXPECT_EQ(child_a_string, expected_child_a); + EXPECT_EQ(child_a_const_string, expected_child_a); + EXPECT_EQ(child_a_path, expected_child_a); + EXPECT_EQ(child_a_const_path, expected_child_a); + + // Test B + Tree* expected_child_b = &tree.children()["B"]; + Tree* child_b_string = tree.GetChild("B"); + const Tree* child_b_const_string = tree.GetChild("B"); + Tree* child_b_path = tree.GetChild(Path("B")); + const Tree* child_b_const_path = tree.GetChild(Path("B")); + EXPECT_EQ(child_b_string, expected_child_b); + EXPECT_EQ(child_b_const_string, expected_child_b); + EXPECT_EQ(child_b_path, expected_child_b); + EXPECT_EQ(child_b_const_path, expected_child_b); + + // Test B/B1 + Tree* expected_child_b_b1 = &tree.children()["B"].children()["B1"]; + Tree* child_b_b1_string = + child_b_string ? child_b_string->GetChild("B1") : nullptr; + const Tree* child_b_b1_const_string = + child_b_const_string ? child_b_const_string->GetChild("B1") : nullptr; + Tree* child_b_b1_path = tree.GetChild(Path("B/B1")); + const Tree* child_b_b1_const_path = tree.GetChild(Path("B/B1")); + EXPECT_EQ(child_b_b1_string, expected_child_b_b1); + EXPECT_EQ(child_b_b1_const_string, expected_child_b_b1); + EXPECT_EQ(child_b_b1_path, expected_child_b_b1); + EXPECT_EQ(child_b_b1_const_path, expected_child_b_b1); + EXPECT_EQ(tree.GetChild("B/B1"), nullptr); + + // Test A/A1 (Does not exist) + Tree* child_a_a1_string = + child_a_string ? child_a_string->GetChild("A1") : nullptr; + const Tree* child_a_a1_const_string = + child_a_const_string ? child_a_const_string->GetChild("A1") : nullptr; + Tree* child_a_a1_path = tree.GetChild(Path("A/A1")); + const Tree* child_a_a1_const_path = tree.GetChild(Path("A/A1")); + EXPECT_EQ(child_a_a1_string, nullptr); + EXPECT_EQ(child_a_a1_const_string, nullptr); + EXPECT_EQ(child_a_a1_path, nullptr); + EXPECT_EQ(child_a_a1_const_path, nullptr); + + // Test C (Does not exist) + Tree* child_c_string = tree.GetChild("C"); + const Tree* child_c_const_string = tree.GetChild("C"); + Tree* child_c_path = tree.GetChild(Path("C")); + const Tree* child_c_const_path = tree.GetChild(Path("C")); + EXPECT_EQ(child_c_string, nullptr); + EXPECT_EQ(child_c_const_string, nullptr); + EXPECT_EQ(child_c_path, nullptr); + EXPECT_EQ(child_c_const_path, nullptr); +} + +TEST(TreeTest, IsEmpty) { + { + Tree tree; + EXPECT_TRUE(tree.IsEmpty()); + } + + { + Tree tree(1); + EXPECT_FALSE(tree.IsEmpty()); + } + + { + Tree tree; + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("A/A1"), 20); + tree.SetValueAt(Path("B/B1"), 30); + EXPECT_FALSE(tree.IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("A"))->IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("A/A1"))->IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("B"))->IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("B/B1"))->IsEmpty()); + } +} + +TEST(TreeTest, GetOrMakeSubtree) { + Tree tree; + Tree* subtree; + tree.SetValueAt(Path("aaa/bbb/ccc"), 100); + + // Get existing subtree. + subtree = tree.GetOrMakeSubtree(Path("aaa/bbb/ccc")); + EXPECT_EQ(subtree->value().value(), 100); + + // Make new subtree. + subtree = tree.GetOrMakeSubtree(Path("zzz/yyy/xxx")); + EXPECT_NE(subtree, nullptr); + EXPECT_FALSE(subtree->value().has_value()); + // Now set the value, and verify the pointer we're holding updated + // appropriately. + tree.SetValueAt(Path("zzz/yyy/xxx"), 200); + EXPECT_TRUE(subtree->value().has_value()); + EXPECT_EQ(subtree->value().value(), 200); + + // Make new subtree along an exsiting path. + subtree = tree.GetOrMakeSubtree(Path("aaa/bbb/mmm")); + EXPECT_NE(subtree, nullptr); + EXPECT_FALSE(subtree->value().has_value()); + // Now set the value, and verify the pointer we're holding updated + // appropriately. + tree.SetValueAt(Path("aaa/bbb/mmm"), 300); + EXPECT_TRUE(subtree->value().has_value()); + EXPECT_EQ(subtree->value().value(), 300); +} + +TEST(TreeTest, GetPath) { + Tree tree; + const Tree* subtree = tree.GetOrMakeSubtree(Path("aaa/bbb/ccc")); + + EXPECT_EQ(tree.GetPath(), Path()); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); +} + +// Record a list of visited node with its path and value +typedef std::vector> VisitedList; + +// Get a list of visited child node with its path and value +VisitedList GetVisitedChild(const Tree& tree, const Path& input_path) { + VisitedList visited; + + tree.CallOnEach( + input_path, + [](const Path& path, int* value, void* data) { + VisitedList* visited = static_cast(data); + visited->push_back(std::make_pair(path.str(), *value)); + }, + &visited); + return visited; +} + +// Get a list of visited child node with its path and value, using std::function +VisitedList GetVisitedChildStdFunction(const Tree& tree, + const Path& input_path) { + VisitedList visited; + + tree.CallOnEach(input_path, [&](const Path& path, const int& value) { + visited.push_back(std::make_pair(path.str(), value)); + }); + return visited; +} + +TEST(TreeTest, CallOnEach) { + { + Tree tree; + + Path input_path(""); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + + { + Tree tree(0); + + { + Path input_path(""); + VisitedList expected = { + {"", 0}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A"); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + } + + { + Tree tree(0); + tree.SetValueAt(Path("A"), 1); + + { + Path input_path(""); + VisitedList expected = { + {"", 0}, + {"A", 1}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A"); + VisitedList expected = { + {"A", 1}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + } + + { + Tree tree(0); + tree.SetValueAt(Path("A"), 1); + tree.SetValueAt(Path("A/A1"), 10); + tree.SetValueAt(Path("A/A2/A21"), 110); + tree.SetValueAt(Path("B/B1"), 20); + + { + Path input_path(""); + VisitedList expected = { + {"", 0}, {"A", 1}, {"A/A1", 10}, {"A/A2/A21", 110}, {"B/B1", 20}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A"); + VisitedList expected = { + {"A", 1}, + {"A/A1", 10}, + {"A/A2/A21", 110}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A/A1"); + VisitedList expected = { + {"A/A1", 10}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A/A2"); + VisitedList expected = { + {"A/A2/A21", 110}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("B"); + VisitedList expected = { + {"B/B1", 20}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("B/B1"); + VisitedList expected = { + {"B/B1", 20}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + // Does not exist + Path input_path("B/B2"); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + // Does not exist + Path input_path("B/B1/B11"); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + } +} + +TEST(TreeTest, CallOnEachAncestorIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{3, 2, 1}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachAncestor( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachAncestorDoNotIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{2, 1}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachAncestor( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + false); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{4}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant([&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantDoNotIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{3, 4}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantChildrenFirst) { + std::vector call_order; + std::vector expected_call_order{4, 3}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true, true); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantChildrenLast) { + std::vector call_order; + std::vector expected_call_order{3, 4}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true, false); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, FindRootMostPathWithValueSuccess) { + Tree tree; + tree.SetValueAt(Path("1/2/3"), 100); + tree.SetValueAt(Path("1/2/3/4/5/6"), 200); + + Optional result = tree.FindRootMostPathWithValue(Path("1/2/3/4/5/6/7")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), Path("1/2/3")); +} + +TEST(TreeTest, FindRootMostPathWithValueNoValue) { + Tree tree; + tree.SetValueAt(Path("a/b/c"), 100); + tree.SetValueAt(Path("a/b/c/d/e/f"), 200); + + Optional result = tree.FindRootMostPathWithValue(Path("1/2/3/4/5/6/7")); + EXPECT_FALSE(result.has_value()); +} + +TEST(TreeTest, FindRootMostMatchingPathSuccess) { + Tree tree; + tree.SetValueAt(Path("1"), 1); + tree.SetValueAt(Path("1/2"), 3); + tree.SetValueAt(Path("1/2/3"), 6); + tree.SetValueAt(Path("1/2/3/4"), 10); + tree.SetValueAt(Path("1/2/3/4/5"), 15); + tree.SetValueAt(Path("1/2/3/4/5/6"), 21); + + Optional result = tree.FindRootMostMatchingPath( + Path("1/2/3/4/5/6"), [](int value) { return value == 10; }); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), Path("1/2/3/4")); +} + +TEST(TreeTest, FindRootMostMatchingPathNoMatch) { + Tree tree; + tree.SetValueAt(Path("1"), 1); + tree.SetValueAt(Path("1/2"), 3); + tree.SetValueAt(Path("1/2/3"), 6); + tree.SetValueAt(Path("1/2/3/4"), 10); + tree.SetValueAt(Path("1/2/3/4/5"), 15); + tree.SetValueAt(Path("1/2/3/4/5/6"), 21); + + Optional result = tree.FindRootMostMatchingPath( + Path("1/2/3/4/5/6"), [](int value) { return value == 100; }); + EXPECT_FALSE(result.has_value()); +} + +TEST(TreeTest, Fold) { + Tree tree; + tree.SetValueAt(Path("1/1"), 'H'); + tree.SetValueAt(Path("1/2"), 'e'); + tree.SetValueAt(Path("1/3"), 'l'); + tree.SetValueAt(Path("1/4/1"), 'l'); + tree.SetValueAt(Path("1/4"), 'o'); + tree.SetValueAt(Path("1"), ','); + tree.SetValueAt(Path("2"), ' '); + tree.SetValueAt(Path("3/1/1"), 'w'); + tree.SetValueAt(Path("3/1/2"), 'o'); + tree.SetValueAt(Path("3/1"), 'r'); + tree.SetValueAt(Path("3/2"), 'l'); + tree.SetValueAt(Path("3"), 'd'); + tree.SetValueAt(Path("4"), '!'); + + std::string result = tree.Fold( + std::string(), + [](Path path, char value, std::string accum) { return accum += value; }); + + EXPECT_EQ(result, "Hello, world!"); +} + +TEST(TreeTest, Equality) { + Tree tree; + tree.SetValueAt(Path("1/1"), 'H'); + tree.SetValueAt(Path("1/2"), 'e'); + tree.SetValueAt(Path("1/3"), 'l'); + tree.SetValueAt(Path("1/4/1"), 'l'); + tree.SetValueAt(Path("1/4"), 'o'); + tree.SetValueAt(Path("1"), ','); + tree.SetValueAt(Path("2"), ' '); + tree.SetValueAt(Path("3/1/1"), 'w'); + tree.SetValueAt(Path("3/1/2"), 'o'); + tree.SetValueAt(Path("3/1"), 'r'); + tree.SetValueAt(Path("3/2"), 'l'); + tree.SetValueAt(Path("3"), 'd'); + tree.SetValueAt(Path("4"), '!'); + + Tree same_tree; + same_tree.SetValueAt(Path("1/1"), 'H'); + same_tree.SetValueAt(Path("1/2"), 'e'); + same_tree.SetValueAt(Path("1/3"), 'l'); + same_tree.SetValueAt(Path("1/4/1"), 'l'); + same_tree.SetValueAt(Path("1/4"), 'o'); + same_tree.SetValueAt(Path("1"), ','); + same_tree.SetValueAt(Path("2"), ' '); + same_tree.SetValueAt(Path("3/1/1"), 'w'); + same_tree.SetValueAt(Path("3/1/2"), 'o'); + same_tree.SetValueAt(Path("3/1"), 'r'); + same_tree.SetValueAt(Path("3/2"), 'l'); + same_tree.SetValueAt(Path("3"), 'd'); + same_tree.SetValueAt(Path("4"), '!'); + + Tree different_tree; + different_tree.SetValueAt(Path("1/1"), 'H'); + different_tree.SetValueAt(Path("1/2"), 'E'); + different_tree.SetValueAt(Path("1/3"), 'L'); + different_tree.SetValueAt(Path("1/4/1"), 'L'); + different_tree.SetValueAt(Path("1/4"), 'O'); + different_tree.SetValueAt(Path("1"), '!'); + different_tree.SetValueAt(Path("2"), ' '); + different_tree.SetValueAt(Path("3/1/1"), 'w'); + different_tree.SetValueAt(Path("3/1/2"), 'a'); + different_tree.SetValueAt(Path("3/1"), 'r'); + different_tree.SetValueAt(Path("3/2"), 'l'); + different_tree.SetValueAt(Path("3"), 'd'); + different_tree.SetValueAt(Path("4"), '?'); + + EXPECT_EQ(tree, same_tree); + EXPECT_NE(tree, different_tree); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/write_tree_test.cc b/database/tests/desktop/core/write_tree_test.cc new file mode 100644 index 0000000000..7aac190985 --- /dev/null +++ b/database/tests/desktop/core/write_tree_test.cc @@ -0,0 +1,792 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/write_tree.h" + +#include "app/src/variant_util.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/tests/desktop/test/mock_write_tree.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using testing::_; +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(WriteTree, ChildWrites) { + WriteTree write_tree; + WriteTreeRef ref = write_tree.ChildWrites(Path("test/path")); + + EXPECT_EQ(ref.path(), Path("test/path")); + EXPECT_EQ(ref.write_tree(), &write_tree); +} + +TEST(WriteTree, AddOverwrite) { + WriteTree write_tree; + WriteTreeRef ref = write_tree.ChildWrites(Path("test/path")); + + EXPECT_EQ(ref.path(), Path("test/path")); + EXPECT_EQ(ref.write_tree(), &write_tree); +} + +TEST(WriteTreeDeathTest, AddOverwrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path"), snap, 100, kOverwriteVisible); + + UserWriteRecord* record = write_tree.GetWrite(100); + EXPECT_TRUE(record->is_overwrite); + EXPECT_TRUE(record->visible); + EXPECT_EQ(record->path, Path("test/path")); + EXPECT_EQ(record->overwrite, snap); +} + +TEST(WriteTree, AddMerge) { + WriteTree write_tree; + CompoundWrite changed_children; + write_tree.AddMerge(Path("test/path"), changed_children, 100); + + UserWriteRecord* record = write_tree.GetWrite(100); + EXPECT_FALSE(record->is_overwrite); + EXPECT_TRUE(record->visible); + EXPECT_EQ(record->path, Path("test/path")); +} + +TEST(WriteTreeDeathTest, AddMerge) { + WriteTree write_tree; + CompoundWrite changed_children; + write_tree.AddMerge(Path("test/path"), changed_children, 100); + EXPECT_DEATH(write_tree.AddMerge(Path("test/path"), changed_children, 50), + DEATHTEST_SIGABRT); +} + +TEST(WriteTree, GetWrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one"), snap, 100, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two"), snap, 101, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/three"), snap, 102, + kOverwriteVisible); + + EXPECT_EQ(write_tree.GetWrite(99), nullptr); + EXPECT_NE(write_tree.GetWrite(100), nullptr); + EXPECT_EQ(write_tree.GetWrite(100)->path, Path("test/path/one")); + EXPECT_NE(write_tree.GetWrite(101), nullptr); + EXPECT_EQ(write_tree.GetWrite(101)->path, Path("test/path/two")); + EXPECT_NE(write_tree.GetWrite(102), nullptr); + EXPECT_EQ(write_tree.GetWrite(102)->path, Path("test/path/three")); + EXPECT_EQ(write_tree.GetWrite(103), nullptr); +} + +TEST(WriteTree, PurgeAllWrites) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one"), snap, 100, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two"), snap, 101, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/three"), snap, 102, + kOverwriteVisible); + + std::vector purged_writes{ + UserWriteRecord(100, Path("test/path/one"), snap, kOverwriteVisible), + UserWriteRecord(101, Path("test/path/two"), snap, kOverwriteVisible), + UserWriteRecord(102, Path("test/path/three"), snap, kOverwriteVisible), + }; + EXPECT_THAT(write_tree.PurgeAllWrites(), Pointwise(Eq(), purged_writes)); +} + +TEST(WriteTree, RemoveWrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one/visible"), snap, 100, + kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two/invisible"), snap, 101, + kOverwriteInvisible); + write_tree.AddOverwrite(Path("test/path/three/visible"), snap, 102, + kOverwriteVisible); + + // Removing visible write returns true. + EXPECT_TRUE(write_tree.RemoveWrite(100)); + // Removing invisible write returns false. + EXPECT_FALSE(write_tree.RemoveWrite(101)); + + EXPECT_EQ(write_tree.GetWrite(100), nullptr); + EXPECT_EQ(write_tree.GetWrite(101), nullptr); + EXPECT_NE(write_tree.GetWrite(102), nullptr); +} + +TEST(WriteTreeDeathTest, RemoveWrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one/visible"), snap, 100, + kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two/invisible"), snap, 101, + kOverwriteInvisible); + write_tree.AddOverwrite(Path("test/path/three/visible"), snap, 102, + kOverwriteVisible); + + // Cannot remove a write that never happened. + EXPECT_DEATH(write_tree.RemoveWrite(200), DEATHTEST_SIGABRT); +} + +TEST(WriteTree, GetCompleteWriteData) { + WriteTree write_tree; + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + EXPECT_FALSE(write_tree.GetCompleteWriteData(Path()).has_value()); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/aaa")), 1); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/bbb")), 2); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/ddd")), 3); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/eee")), 4); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/fff/ggg")), 5); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/fff/hhh")), 6); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/fff/iii")), + Variant::Null()); + EXPECT_FALSE(write_tree.GetCompleteWriteData(Path("test/fff")).has_value()); + + EXPECT_FALSE(write_tree.ShadowingWrite(Path()).has_value()); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/aaa")), 1); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/bbb")), 2); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/ddd")), 3); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/eee")), 4); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/fff/ggg")), 5); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/fff/hhh")), 6); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/fff/iii")), + Variant::Null()); + EXPECT_FALSE(write_tree.ShadowingWrite(Path("test/fff")).has_value()); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_ShadowingWrite) { + WriteTree write_tree; + Path tree_path("test/ccc"); + Variant complete_server_cache; + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, no_write_ids_to_exclude); + + Variant expected_result(std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_NoChildMerge) { + WriteTree write_tree; + Path tree_path("test/not_present"); + Variant complete_server_cache("server_cache"); + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, no_write_ids_to_exclude); + + Variant expected_result("server_cache"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_NoCompleteSnapshot) { + WriteTree write_tree; + Path tree_path("test/not_present"); + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, nullptr, no_write_ids_to_exclude); + + EXPECT_FALSE(result.has_value()); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_ApplyCache) { + WriteTree write_tree; + Path tree_path("test"); + Variant complete_server_cache(std::map{ + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", -3), + std::make_pair("fff", 5), + }), + }); + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, no_write_ids_to_exclude); + + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + std::make_pair("fff", 5), + }), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, + CalcCompleteEventCache_HasExcludes_NoHiddenWritesAndEmptyMerge) { + WriteTree write_tree; + Path tree_path("test/not_present"); + Variant complete_server_cache("server_cache"); + std::vector write_ids_to_exclude{95}; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, write_ids_to_exclude); + + Variant expected_result("server_cache"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventCache_HasExcludes_NoHiddenWritesAndMergeData) { + WriteTree write_tree; + Path tree_path("test"); + Variant complete_server_cache(std::map{ + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", -3), + std::make_pair("fff", 5), + }), + }); + std::vector write_ids_to_exclude{101, 102}; + const std::map& merge_100{std::make_pair(Path("aaa"), 1)}; + const std::map& merge_101{std::make_pair(Path("bbb"), 2)}; + const std::map& merge_102{std::make_pair(Path("ccc/ddd"), 3)}; + const std::map& merge_103{std::make_pair(Path("ccc/eee"), 4)}; + CompoundWrite write_100 = CompoundWrite::FromPathMerge(merge_100); + CompoundWrite write_101 = CompoundWrite::FromPathMerge(merge_101); + CompoundWrite write_102 = CompoundWrite::FromPathMerge(merge_102); + CompoundWrite write_103 = CompoundWrite::FromPathMerge(merge_103); + write_tree.AddMerge(Path("test"), write_100, 100); + write_tree.AddMerge(Path("test"), write_101, 101); + write_tree.AddMerge(Path("test"), write_102, 102); + write_tree.AddMerge(Path("test"), write_103, 103); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, write_ids_to_exclude); + + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", -3), + std::make_pair("eee", 4), + std::make_pair("fff", 5), + }), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventChildren_WithTopLevelSet) { + WriteTree write_tree; + Path tree_path("test/ccc"); + Variant complete_server_children("Irrelevant"); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Variant result = + write_tree.CalcCompleteEventChildren(tree_path, complete_server_children); + Variant expected_result(std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }); + EXPECT_EQ(result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventChildren_WithoutTopLevelSet) { + WriteTree write_tree; + Path tree_path("test"); + Variant complete_server_children(std::map{ + std::make_pair("zzz", -1), + std::make_pair("yyy", -2), + }); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Variant result = + write_tree.CalcCompleteEventChildren(tree_path, complete_server_children); + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + std::make_pair("zzz", -1), + std::make_pair("yyy", -2), + }); + + EXPECT_EQ(result, expected_result); +} + +TEST(WriteTree, CalcEventCacheAfterServerOverwrite_NoWritesAreShadowing) { + WriteTree write_tree; + Path tree_path("test/ccc"); + Path child_path("ddd"); + Variant existing_local_snap; + Variant exsiting_server_snap(std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + // Given that the underlying server data has updated, determine what, if + // anything, needs to be applied to the event cache. In this case, no writes + // are shadowing. Events should be raised, the snap to be applied comes from + // the server data. + Optional result = write_tree.CalcEventCacheAfterServerOverwrite( + tree_path, child_path, &existing_local_snap, &exsiting_server_snap); + Variant expected_result = 3; + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcEventCacheAfterServerOverwrite_CompleteShadowing) { + WriteTree write_tree; + Path tree_path("test"); + Path child_path("aaa"); + Variant existing_local_snap; + Variant exsiting_server_snap; + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + // Given that the underlying server data has updated, determine what, if + // anything, needs to be applied to the event cache. The write at "test/aaa" + // is completely shadowed by what is already in the tree. + Optional result = write_tree.CalcEventCacheAfterServerOverwrite( + tree_path, child_path, &existing_local_snap, &exsiting_server_snap); + + EXPECT_FALSE(result.has_value()); +} + +TEST(WriteTree, CalcEventCacheAfterServerOverwrite_PartiallyShadowed) { + WriteTree write_tree; + Path tree_path("test"); + Path child_path; + Variant existing_local_snap; + Variant exsiting_server_snap(std::map{ + std::make_pair("zzz", 100), + }); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + // Given that the underlying server data has updated, determine what, if + // anything, needs to be applied to the event cache. The write at "test" is + // partially shadowed, so we'll need to merge the server snap with the write + // to get the updated snapshot. + Optional result = write_tree.CalcEventCacheAfterServerOverwrite( + tree_path, child_path, &existing_local_snap, &exsiting_server_snap); + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + std::make_pair("zzz", 100), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTreeDeathTest, CalcEventCacheAfterServerOverwrite) { + WriteTree write_tree; + EXPECT_DEATH(write_tree.CalcEventCacheAfterServerOverwrite(Path(), Path(), + nullptr, nullptr), + DEATHTEST_SIGABRT); +} + +TEST(WriteTree, CalcCompleteChild_HasShadowingVariant) { + WriteTree write_tree; + Path tree_path("test"); + std::string child_key("aaa"); + CacheNode existing_server_cache; + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Optional result = + write_tree.CalcCompleteChild(tree_path, child_key, existing_server_cache); + Variant expected_result = 1; + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteChild_HasCompleteChild) { + WriteTree write_tree; + Path tree_path("test"); + std::string child_key("bbb"); + CacheNode existing_server_cache( + IndexedVariant(std::map{std::make_pair("bbb", 2)}), + true, false); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Optional result = + write_tree.CalcCompleteChild(tree_path, child_key, existing_server_cache); + Variant expected_result = 2; + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteChild_NoCompleteChild) { + WriteTree write_tree; + Path tree_path("test"); + std::string child_key("ccc"); + CacheNode existing_server_cache( + IndexedVariant(std::map{std::make_pair("bbb", 2)}), + true, false); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Variant expected_result; + Optional result = + write_tree.CalcCompleteChild(tree_path, child_key, existing_server_cache); + + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcNextVariantAfterPost_WithShadowingVariant) { + WriteTree write_tree; + write_tree.AddOverwrite(Path("test"), + Variant(std::map{ + std::make_pair("aaa", 5), + std::make_pair("bbb", 4), + std::make_pair("ccc", 3), + std::make_pair("ddd", 2), + std::make_pair("eee", 1), + }), + 101, kOverwriteVisible); + + Path tree_path("test"); + Optional complete_server_data; + IterationDirection direction = kIterateForward; + QuerySpec query_spec; + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("aaa", 5), + direction, query_spec.params), + std::make_pair(Variant("bbb"), Variant(4))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("bbb", 4), + direction, query_spec.params), + std::make_pair(Variant("ccc"), Variant(3))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ccc", 3), + direction, query_spec.params), + std::make_pair(Variant("ddd"), Variant(2))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ddd", 2), + direction, query_spec.params), + std::make_pair(Variant("eee"), Variant(1))); + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("eee", 1), + direction, query_spec.params) + .has_value()); +} + +TEST(WriteTree, CalcNextVariantAfterPost_WithoutShadowingVariant) { + WriteTree write_tree; + Path tree_path("test"); + Optional complete_server_data(std::map{ + std::make_pair("aaa", 5), + std::make_pair("bbb", 4), + std::make_pair("ccc", 3), + std::make_pair("ddd", 2), + std::make_pair("eee", 1), + }); + IterationDirection direction = kIterateForward; + QuerySpec query_spec; + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("aaa", 5), + direction, query_spec.params), + std::make_pair(Variant("bbb"), Variant(4))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("bbb", 4), + direction, query_spec.params), + std::make_pair(Variant("ccc"), Variant(3))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ccc", 3), + direction, query_spec.params), + std::make_pair(Variant("ddd"), Variant(2))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ddd", 2), + direction, query_spec.params), + std::make_pair(Variant("eee"), Variant(1))); + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("eee", 1), + direction, query_spec.params) + .has_value()); +} + +TEST(WriteTree, CalcNextVariantAfterPost_WithoutShadowingVariantOrServerData) { + WriteTree write_tree; + Path tree_path("test"); + Optional complete_server_data; + IterationDirection direction = kIterateForward; + QuerySpec query_spec; + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("aaa", 5), + direction, query_spec.params) + .has_value()); +} + +TEST(WriteTree, CalcNextVariantAfterPost_Reverse) { + WriteTree write_tree; + write_tree.AddOverwrite(Path("test"), + Variant(std::map{ + std::make_pair("aaa", 5), + std::make_pair("bbb", 4), + std::make_pair("ccc", 3), + std::make_pair("ddd", 2), + std::make_pair("eee", 1), + }), + 101, kOverwriteVisible); + + Path tree_path("test"); + Optional complete_server_data; + IterationDirection direction = kIterateReverse; + QuerySpec query_spec; + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("aaa", 5), + kIterateReverse, query_spec.params) + .has_value()); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("bbb", 4), + direction, query_spec.params), + std::make_pair(Variant("aaa"), Variant(5))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ccc", 3), + direction, query_spec.params), + std::make_pair(Variant("bbb"), Variant(4))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ddd", 2), + direction, query_spec.params), + std::make_pair(Variant("ccc"), Variant(3))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("eee", 1), + direction, query_spec.params), + std::make_pair(Variant("ddd"), Variant(2))); +} + +TEST(WriteTreeRef, Constructor) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + + EXPECT_EQ(ref.path(), Path("test/path")); + EXPECT_EQ(ref.write_tree(), &write_tree); +} + +TEST(WriteTreeRef, CalcCompleteEventCache1) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Variant complete_server_cache; + + EXPECT_CALL(write_tree, CalcCompleteEventCache(Eq(Path("test/path")), + Eq(&complete_server_cache))); + ref.CalcCompleteEventCache(&complete_server_cache); +} + +TEST(WriteTreeRef, CalcCompleteEventCache2) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Variant complete_server_cache; + std::vector write_ids_to_exclude; + + EXPECT_CALL(write_tree, CalcCompleteEventCache(Eq(Path("test/path")), + Eq(&complete_server_cache), + Eq(write_ids_to_exclude))); + ref.CalcCompleteEventCache(&complete_server_cache, write_ids_to_exclude); +} + +TEST(WriteTreeRef, CalcCompleteEventCache3) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Variant complete_server_cache; + std::vector write_ids_to_exclude; + HiddenWriteInclusion include_hidden_writes = kExcludeHiddenWrites; + + EXPECT_CALL(write_tree, CalcCompleteEventCache(Eq(Path("test/path")), + Eq(&complete_server_cache), + Eq(write_ids_to_exclude), + Eq(include_hidden_writes))); + ref.CalcCompleteEventCache(&complete_server_cache, write_ids_to_exclude, + include_hidden_writes); +} + +TEST(WriteTreeRef, CalcEventCacheAfterServerOverwrite) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Path path("another/path"); + Variant existing_local_snap; + Variant existing_server_snap; + + EXPECT_CALL(write_tree, + CalcEventCacheAfterServerOverwrite( + Eq(Path("test/path")), Eq(Path("another/path")), + Eq(&existing_local_snap), Eq(&existing_server_snap))); + ref.CalcEventCacheAfterServerOverwrite(path, &existing_local_snap, + &existing_server_snap); +} + +TEST(WriteTreeRef, ShadowingWrite) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Path path("another/path"); + + EXPECT_CALL(write_tree, ShadowingWrite(Eq(Path("test/path/another/path")))); + ref.ShadowingWrite(path); +} + +TEST(WriteTreeRef, CalcCompleteChild) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + CacheNode existing_server_cache; + + EXPECT_CALL(write_tree, + CalcCompleteChild(Eq(Path("test/path")), Eq("child_key"), _)); + ref.CalcCompleteChild("child_key", existing_server_cache); +} + +TEST(WriteTreeRef, Child) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + + WriteTreeRef child_ref = ref.Child("child_key"); + + EXPECT_EQ(child_ref.path(), Path("test/path/child_key")); + EXPECT_EQ(child_ref.write_tree(), &write_tree); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/mutable_data_desktop_test.cc b/database/tests/desktop/mutable_data_desktop_test.cc new file mode 100644 index 0000000000..3af7c11415 --- /dev/null +++ b/database/tests/desktop/mutable_data_desktop_test.cc @@ -0,0 +1,237 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/mutable_data_desktop.h" + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; + +namespace firebase { +namespace database { +namespace internal { + +TEST(MutableDataTest, TestBasic) { + { + MutableDataInternal data(nullptr, Variant::Null()); + EXPECT_THAT(data.GetChildren().size(), Eq(0)); + EXPECT_THAT(data.GetChildrenCount(), Eq(0)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), Eq(Variant::Null())); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_FALSE(data.HasChild("A")); + } + + { + MutableDataInternal data(nullptr, Variant(10)); + EXPECT_THAT(data.GetChildren().size(), Eq(0)); + EXPECT_THAT(data.GetChildrenCount(), Eq(0)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_FALSE(data.HasChild("A")); + } + + { + MutableDataInternal data( + nullptr, util::JsonToVariant("{\".value\":10,\".priority\":1}")); + EXPECT_THAT(data.GetChildren().size(), Eq(0)); + EXPECT_THAT(data.GetChildrenCount(), Eq(0)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_FALSE(data.HasChild("A")); + } + + { + MutableDataInternal data( + nullptr, + util::JsonToVariant("{\"A\":{\"B\":{\"C\":10}},\".priority\":1}")); + EXPECT_THAT(data.GetChildren().size(), Eq(1)); + EXPECT_THAT(data.GetChildrenCount(), Eq(1)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), + Eq(util::JsonToVariant("{\"A\":{\"B\":{\"C\":10}}}"))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_TRUE(data.HasChild("A")); + EXPECT_TRUE(data.HasChild("A/B")); + EXPECT_TRUE(data.HasChild("A/B/C")); + EXPECT_FALSE(data.HasChild("A/B/C/D")); + EXPECT_FALSE(data.HasChild("D")); + + auto child_a = data.Child("A"); + EXPECT_THAT(child_a->GetChildren().size(), Eq(1)); + EXPECT_THAT(child_a->GetChildrenCount(), Eq(1)); + EXPECT_THAT(child_a->GetKeyString(), Eq("A")); + EXPECT_THAT(child_a->GetValue(), + Eq(util::JsonToVariant("{\"B\":{\"C\":10}}"))); + EXPECT_THAT(child_a->GetPriority(), Eq(Variant::Null())); + EXPECT_TRUE(child_a->HasChild("B")); + EXPECT_TRUE(child_a->HasChild("B/C")); + EXPECT_FALSE(child_a->HasChild("B/C/D")); + EXPECT_FALSE(child_a->HasChild("D")); + + delete child_a; + } +} + +TEST(MutableDataTest, TestWrite) { + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue(Variant(10)); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), Eq(Variant(10))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetPriority(Variant(1)); + EXPECT_THAT(data.GetValue(), Eq(Variant::Null())); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), Eq(Variant::Null())); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue(Variant(10)); + data.SetPriority(Variant(1)); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\".value\":10}"))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetPriority(Variant(1)); + data.SetValue(Variant(10)); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), Eq(util::JsonToVariant("10"))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue(util::JsonToVariant("{\"A\":10,\"B\":20}")); + data.SetPriority(Variant(1)); + EXPECT_THAT(data.GetValue(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"A\":10,\"B\":20}"))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetPriority(Variant(1)); + data.SetValue(util::JsonToVariant("{\"A\":10,\"B\":20}")); + EXPECT_THAT(data.GetValue(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + } +} + +TEST(MutableDataTest, TestChild) { + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child_a = data.Child("A"); + child_a->SetValue(Variant(10)); + auto child_b = data.Child("B"); + child_b->SetValue(Variant(20)); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + + delete child_a; + delete child_b; + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child_a = data.Child("A"); + child_a->SetValue(Variant(10)); + auto child_b = child_a->Child("B"); + child_b->SetValue(Variant(20)); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":{\"B\":20}}"))); + + delete child_a; + delete child_b; + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child = data.Child("A/B"); + child->SetValue(Variant(20)); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":{\"B\":20}}"))); + + delete child; + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child_1 = data.Child("A/B/C"); + child_1->SetValue(Variant(20)); + child_1->SetPriority(Variant(3)); + auto child_2 = data.Child("A"); + child_2->SetPriority(Variant(2)); + data.SetPriority(Variant(1)); + EXPECT_THAT( + data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"A\":{\".priority\":2,\"B\":{" + "\"C\":{\".priority\":3,\".value\":20}}}}"))); + + delete child_1; + delete child_2; + } + + { + // Test GetValue() to convert applicable map to vector + MutableDataInternal data(nullptr, Variant::Null()); + auto child_1 = data.Child("0"); + child_1->SetValue(0); + auto child_2 = data.Child("2"); + child_2->SetValue(2); + child_2->SetPriority(20); + data.SetPriority(1); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"0\":0,\"2\":{\"." + "value\":2,\".priority\":20}}"))); + EXPECT_THAT(data.GetValue(), Eq(util::JsonToVariant("[0,null,2]"))); + + delete child_1; + delete child_2; + } + + { + // Set value with vector and priority + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue( + util::JsonToVariant("{\".priority\":1,\".value\":[0,null,{\".value\":2," + "\".priority\":20}]}")); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"0\":0,\"2\":{\"." + "value\":2,\".priority\":20}}"))); + EXPECT_THAT(data.GetValue(), Eq(util::JsonToVariant("[0,null,2]"))); + } +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/flatbuffer_conversions_test.cc b/database/tests/desktop/persistence/flatbuffer_conversions_test.cc new file mode 100644 index 0000000000..d09ddeb94f --- /dev/null +++ b/database/tests/desktop/persistence/flatbuffer_conversions_test.cc @@ -0,0 +1,458 @@ +// Copyright 2020 Google LLC +// +// 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 "database/src/desktop/persistence/flatbuffer_conversions.h" + +#include +#include + +#include "app/src/include/firebase/variant.h" +#include "app/src/variant_util.h" +#include "app/tests/flexbuffer_matcher.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/src/desktop/persistence/persisted_compound_write_generated.h" +#include "database/src/desktop/persistence/persisted_query_params_generated.h" +#include "database/src/desktop/persistence/persisted_query_spec_generated.h" +#include "database/src/desktop/persistence/persisted_tracked_query_generated.h" +#include "database/src/desktop/persistence/persisted_user_write_record_generated.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" + +using firebase::database::internal::persistence::CreatePersistedCompoundWrite; +using firebase::database::internal::persistence::CreatePersistedQueryParams; +using firebase::database::internal::persistence::CreatePersistedQuerySpec; +using firebase::database::internal::persistence::CreatePersistedTrackedQuery; +using firebase::database::internal::persistence::CreateTreeKeyValuePair; +using firebase::database::internal::persistence::CreateVariantTreeNode; + +using firebase::database::internal::persistence:: + FinishPersistedCompoundWriteBuffer; +using firebase::database::internal::persistence:: + FinishPersistedQueryParamsBuffer; +using firebase::database::internal::persistence::FinishPersistedQuerySpecBuffer; +using firebase::database::internal::persistence:: + FinishPersistedTrackedQueryBuffer; +using firebase::database::internal::persistence:: + FinishPersistedUserWriteRecordBuffer; + +using firebase::util::VariantToFlexbuffer; +using flatbuffers::FlatBufferBuilder; +using flatbuffers::Offset; + +// This makes it easier to understand what all the 0's mean. +static const int kFlatbufferEmptyField = 0; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// This is just to make some of the tests clearer. +typedef std::vector> + VectorOfKeyValuePairs; + +TEST(FlatBufferConversion, CompoundWriteFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedCompoundWriteBuffer( + builder, + CreatePersistedCompoundWrite(builder, CreateVariantTreeNode( + builder, + kFlatbufferEmptyField, + builder.CreateVector(VectorOfKeyValuePairs{ + CreateTreeKeyValuePair( + builder, + builder.CreateString("aaa"), + CreateVariantTreeNode( + builder, + kFlatbufferEmptyField, + builder.CreateVector(VectorOfKeyValuePairs{ + CreateTreeKeyValuePair( + builder, + builder.CreateString("bbb"), + CreateVariantTreeNode( + builder, + builder.CreateVector( + VariantToFlexbuffer(100)), + kFlatbufferEmptyField)) + }) + ) + ) + }) + )) + ); + // clang-format on + + const persistence::PersistedCompoundWrite* persisted_compound_write = + persistence::GetPersistedCompoundWrite(builder.GetBufferPointer()); + CompoundWrite result = CompoundWriteFromFlatbuffer(persisted_compound_write); + + CompoundWrite expected_result; + expected_result.AddWriteInline(Path("aaa/bbb"), 100); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, QueryParamsFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedQueryParamsBuffer( + builder, + persistence::CreatePersistedQueryParams( + builder, + persistence::OrderBy_Value, + builder.CreateString("order_by_child"), + builder.CreateVector(VariantToFlexbuffer(1234)), + builder.CreateString("start_at"), + builder.CreateVector(VariantToFlexbuffer(9876)), + builder.CreateString("end_at"), + builder.CreateVector(VariantToFlexbuffer(5555)), + builder.CreateString("equal_to"), + 3333, + 6666)); + // clang-format on + + const persistence::PersistedQueryParams* persisted_query_params = + persistence::GetPersistedQueryParams(builder.GetBufferPointer()); + QueryParams result = QueryParamsFromFlatbuffer(persisted_query_params); + + QueryParams expected_result; + expected_result.order_by = QueryParams::kOrderByValue; + expected_result.order_by_child = "order_by_child"; + expected_result.start_at_value = Variant::FromInt64(1234); + expected_result.start_at_child_key = "start_at"; + expected_result.end_at_value = Variant::FromInt64(9876); + expected_result.end_at_child_key = "end_at"; + expected_result.equal_to_value = Variant::FromInt64(5555); + expected_result.equal_to_child_key = "equal_to"; + expected_result.limit_first = 3333; + expected_result.limit_last = 6666; + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, QuerySpecFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedQuerySpecBuffer( + builder, + CreatePersistedQuerySpec( + builder, + builder.CreateString("this/is/a/path/to/a/thing"), + CreatePersistedQueryParams( + builder, + persistence::OrderBy_Value, + builder.CreateString("order_by_child"), + builder.CreateVector(VariantToFlexbuffer(1234)), + builder.CreateString("start_at"), + builder.CreateVector(VariantToFlexbuffer(9876)), + builder.CreateString("end_at"), + builder.CreateVector(VariantToFlexbuffer(5555)), + builder.CreateString("equal_to"), + 3333, + 6666))); + // clang-format on + + const persistence::PersistedQuerySpec* persisted_query_spec = + persistence::GetPersistedQuerySpec(builder.GetBufferPointer()); + QuerySpec result = QuerySpecFromFlatbuffer(persisted_query_spec); + + QuerySpec expected_result; + expected_result.params.order_by = QueryParams::kOrderByValue; + expected_result.params.order_by_child = "order_by_child"; + expected_result.params.start_at_value = Variant::FromInt64(1234); + expected_result.params.start_at_child_key = "start_at"; + expected_result.params.end_at_value = Variant::FromInt64(9876); + expected_result.params.end_at_child_key = "end_at"; + expected_result.params.equal_to_value = Variant::FromInt64(5555); + expected_result.params.equal_to_child_key = "equal_to"; + expected_result.params.limit_first = 3333; + expected_result.params.limit_last = 6666; + expected_result.path = Path("this/is/a/path/to/a/thing"); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, TrackedQueryFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedTrackedQueryBuffer( + builder, + CreatePersistedTrackedQuery( + builder, + 9999, + CreatePersistedQuerySpec( + builder, + builder.CreateString("some/path"), + CreatePersistedQueryParams( + builder, + persistence::OrderBy_Value, + builder.CreateString("order_by_child"), + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + 0, + 0)), + 543024000, + false, + true)); + // clang-format on + + const persistence::PersistedTrackedQuery* persisted_query_spec = + persistence::GetPersistedTrackedQuery(builder.GetBufferPointer()); + TrackedQuery result = TrackedQueryFromFlatbuffer(persisted_query_spec); + + TrackedQuery expected_result; + expected_result.query_id = 9999; + expected_result.query_spec.params.order_by = QueryParams::kOrderByValue; + expected_result.query_spec.params.order_by_child = "order_by_child"; + expected_result.query_spec.path = Path("some/path"); + expected_result.last_use = 543024000; + expected_result.complete = false; + expected_result.active = true; + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, UserWriteRecordFromFlatbuffer_Overwrite) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedUserWriteRecordBuffer( + builder, + persistence::CreatePersistedUserWriteRecord( + builder, + 1234, + builder.CreateString("this/is/a/path/to/a/thing"), + builder.CreateVector(VariantToFlexbuffer("flexbuffer")), + kFlatbufferEmptyField, + true, + true)); + // clang-format on + + const persistence::PersistedUserWriteRecord* persisted_user_write_record = + persistence::GetPersistedUserWriteRecord(builder.GetBufferPointer()); + UserWriteRecord result = + UserWriteRecordFromFlatbuffer(persisted_user_write_record); + + UserWriteRecord expected_result(1234, Path("this/is/a/path/to/a/thing"), + Variant::FromStaticString("flexbuffer"), + true); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, UserWriteRecordFromFlatbuffer_Merge) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedUserWriteRecordBuffer( + builder, + persistence::CreatePersistedUserWriteRecord( + builder, + 1234, + builder.CreateString("this/is/a/path/to/a/thing"), + kFlatbufferEmptyField, + CreatePersistedCompoundWrite( + builder, + CreateVariantTreeNode( + builder, + kFlatbufferEmptyField, + builder.CreateVector( + VectorOfKeyValuePairs{ + CreateTreeKeyValuePair( + builder, + builder.CreateString("aaa"), + CreateVariantTreeNode( + builder, + builder.CreateVector(VariantToFlexbuffer(100)), + kFlatbufferEmptyField)) + }))), + true, + false)); + // clang-format on + + const persistence::PersistedUserWriteRecord* persisted_user_write_record = + persistence::GetPersistedUserWriteRecord(builder.GetBufferPointer()); + UserWriteRecord result = + UserWriteRecordFromFlatbuffer(persisted_user_write_record); + + UserWriteRecord expected_result( + 1234, Path("this/is/a/path/to/a/thing"), + CompoundWrite::FromPathMerge( + std::map{{Path("aaa"), Variant(100)}})); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, FlatbufferFromPersistedCompoundWrite) { + FlatBufferBuilder builder; + + FinishPersistedCompoundWriteBuffer( + builder, + FlatbufferFromCompoundWrite( + &builder, CompoundWrite::FromPathMerge(std::map{ + {Path("aaa/bbb"), Variant(100)}}))); + + const persistence::PersistedCompoundWrite* result = + persistence::GetPersistedCompoundWrite(builder.GetBufferPointer()); + + EXPECT_EQ(result->write_tree()->value(), nullptr); + + EXPECT_EQ(result->write_tree()->children()->size(), 1); + const persistence::TreeKeyValuePair* node_aaa = + result->write_tree()->children()->Get(0); + EXPECT_STREQ(node_aaa->key()->c_str(), "aaa"); + EXPECT_EQ(node_aaa->subtree()->value(), nullptr); + + EXPECT_EQ(node_aaa->subtree()->children()->size(), 1); + const persistence::TreeKeyValuePair* node_bbb = + node_aaa->subtree()->children()->Get(0); + EXPECT_STREQ(node_bbb->key()->c_str(), "bbb"); + EXPECT_THAT(node_bbb->subtree()->value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(100))); +} + +TEST(FlatBufferConversion, FlatbufferFromQueryParams) { + QueryParams query_params; + query_params.order_by = QueryParams::kOrderByValue; + query_params.order_by_child = "order_by_child"; + query_params.start_at_value = Variant::FromInt64(1234); + query_params.start_at_child_key = "start_at"; + query_params.end_at_value = Variant::FromInt64(9876); + query_params.end_at_child_key = "end_at"; + query_params.equal_to_value = Variant::FromInt64(5555); + query_params.equal_to_child_key = "equal_to"; + query_params.limit_first = 3333; + query_params.limit_last = 6666; + FlatBufferBuilder builder; + + FinishPersistedQueryParamsBuffer( + builder, FlatbufferFromQueryParams(&builder, query_params)); + + const persistence::PersistedQueryParams* result = + persistence::GetPersistedQueryParams(builder.GetBufferPointer()); + + EXPECT_EQ(result->order_by(), persistence::OrderBy_Value); + EXPECT_STREQ(result->order_by_child()->c_str(), "order_by_child"); + EXPECT_THAT(result->start_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(1234))); + EXPECT_STREQ(result->start_at_child_key()->c_str(), "start_at"); + EXPECT_THAT(result->end_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(9876))); + EXPECT_STREQ(result->end_at_child_key()->c_str(), "end_at"); + EXPECT_THAT(result->equal_to_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(5555))); + EXPECT_STREQ(result->equal_to_child_key()->c_str(), "equal_to"); + EXPECT_EQ(result->limit_first(), 3333); + EXPECT_EQ(result->limit_last(), 6666); +} + +TEST(FlatBufferConversion, FlatbufferFromQuerySpec) { + Path path("this/is/a/test/path"); + QueryParams query_params; + query_params.order_by = QueryParams::kOrderByValue; + query_params.order_by_child = "order_by_child"; + query_params.start_at_value = Variant::FromInt64(1234); + query_params.start_at_child_key = "start_at"; + query_params.end_at_value = Variant::FromInt64(9876); + query_params.end_at_child_key = "end_at"; + query_params.equal_to_value = Variant::FromInt64(5555); + query_params.equal_to_child_key = "equal_to"; + query_params.limit_first = 3333; + query_params.limit_last = 6666; + FlatBufferBuilder builder; + QuerySpec query_spec(path, query_params); + + FinishPersistedQuerySpecBuffer(builder, + FlatbufferFromQuerySpec(&builder, query_spec)); + + const persistence::PersistedQuerySpec* result = + persistence::GetPersistedQuerySpec(builder.GetBufferPointer()); + + EXPECT_STREQ(result->path()->c_str(), "this/is/a/test/path"); + EXPECT_EQ(result->params()->order_by(), persistence::OrderBy_Value); + EXPECT_STREQ(result->params()->order_by_child()->c_str(), "order_by_child"); + EXPECT_THAT(result->params()->start_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(1234))); + EXPECT_STREQ(result->params()->start_at_child_key()->c_str(), "start_at"); + EXPECT_THAT(result->params()->end_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(9876))); + EXPECT_STREQ(result->params()->end_at_child_key()->c_str(), "end_at"); + EXPECT_THAT(result->params()->equal_to_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(5555))); + EXPECT_STREQ(result->params()->equal_to_child_key()->c_str(), "equal_to"); + EXPECT_EQ(result->params()->limit_first(), 3333); + EXPECT_EQ(result->params()->limit_last(), 6666); +} + +TEST(FlatBufferConversion, FlatbufferFromTrackedQuery) { + FlatBufferBuilder builder; + TrackedQuery tracked_query; + tracked_query.query_id = 100; + tracked_query.query_spec.path = Path("aaa/bbb/ccc"); + tracked_query.query_spec.params.order_by = QueryParams::kOrderByValue; + tracked_query.last_use = 1234; + tracked_query.complete = true; + tracked_query.active = true; + + FinishPersistedTrackedQueryBuffer( + builder, FlatbufferFromTrackedQuery(&builder, tracked_query)); + + const persistence::PersistedTrackedQuery* result = + persistence::GetPersistedTrackedQuery(builder.GetBufferPointer()); + + EXPECT_EQ(result->query_id(), 100); + EXPECT_STREQ(result->query_spec()->path()->c_str(), "aaa/bbb/ccc"); + EXPECT_EQ(result->query_spec()->params()->order_by(), + persistence::OrderBy_Value); + EXPECT_EQ(result->last_use(), 1234); + EXPECT_TRUE(result->complete()); + EXPECT_TRUE(result->active()); +} + +TEST(FlatBufferConversion, FlatbufferFromUserWriteRecord) { + FlatBufferBuilder builder; + UserWriteRecord user_write_record; + user_write_record.write_id = 123; + user_write_record.path = Path("aaa/bbb/ccc"); + user_write_record.overwrite = Variant("this is a string"); + user_write_record.visible = true; + user_write_record.is_overwrite = true; + + FinishPersistedUserWriteRecordBuffer( + builder, FlatbufferFromUserWriteRecord(&builder, user_write_record)); + const persistence::PersistedUserWriteRecord* result = + persistence::GetPersistedUserWriteRecord(builder.GetBufferPointer()); + + EXPECT_EQ(result->write_id(), 123); + EXPECT_STREQ(result->path()->c_str(), "aaa/bbb/ccc"); + EXPECT_THAT(result->overwrite_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer("this is a string"))); + EXPECT_EQ(result->visible(), true); + EXPECT_EQ(result->is_overwrite(), true); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc b/database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc new file mode 100644 index 0000000000..da0982d14c --- /dev/null +++ b/database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc @@ -0,0 +1,415 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/persistence/in_memory_persistence_storage_engine.h" + +#include "app/src/logger.h" +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/compound_write.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(InMemoryPersistenceStorageEngine, Constructor) { + SystemLogger logger; + InMemoryPersistenceStorageEngine engine(&logger); + + // Ensure there is no crash. + (void)engine; +} + +class InMemoryPersistenceStorageEngineTest : public ::testing::Test { + public: + InMemoryPersistenceStorageEngineTest() : logger_(), engine_(&logger_) {} + + ~InMemoryPersistenceStorageEngineTest() override {} + + protected: + SystemLogger logger_; + InMemoryPersistenceStorageEngine engine_; +}; + +typedef InMemoryPersistenceStorageEngineTest + InMemoryPersistenceStorageEngineDeathTest; + +TEST_F(InMemoryPersistenceStorageEngineTest, LoadServerCache) { + // This is all in-memory, so nothing to read from disk. + EXPECT_EQ(engine_.LoadServerCache(), Variant::Null()); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveUserOverwrite) { + EXPECT_DEATH(engine_.SaveUserOverwrite(Path(), Variant::Null(), 100), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, SaveUserOverwrite) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.SaveUserOverwrite(Path(), Variant::Null(), 100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveUserMerge) { + EXPECT_DEATH(engine_.SaveUserMerge(Path(), CompoundWrite(), 100), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, SaveUserMerge) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.SaveUserMerge(Path(), CompoundWrite(), 100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, RemoveUserWrite) { + EXPECT_DEATH(engine_.RemoveUserWrite(100), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, RemoveUserWrite) { + engine_.BeginTransaction(); + engine_.RemoveUserWrite(100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, LoadUserWrites) { + // This is all in-memory, so nothing to read from disk. + EXPECT_TRUE(engine_.LoadUserWrites().empty()); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, RemoveAllUserWrites) { + // Must be in a transaction. + EXPECT_DEATH(engine_.RemoveAllUserWrites(), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, RemoveAllUserWrites) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.RemoveAllUserWrites(); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, OverwriteServerCache) { + EXPECT_DEATH(engine_.OverwriteServerCache(Path(), Variant::Null()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, OverwriteServerCache) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaa/bbb/ccc"), 100); + engine_.OverwriteServerCache(Path("aaa/bbb/ddd"), 200); + engine_.OverwriteServerCache(Path("zzz/yyy/xxx"), 300); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ccc")), 100); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ddd")), 200); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb")), + Variant(std::map{{"ccc", 100}, {"ddd", 200}})); + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 100}, + {"ddd", 200}, + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }})); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, + MergeIntoServerCache_Variant) { + EXPECT_DEATH(engine_.MergeIntoServerCache(Path(), Variant::Null()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, MergeIntoServerCache_Variant) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaa/bbb/ccc"), 100); + engine_.OverwriteServerCache(Path("aaa/bbb/ddd"), 200); + engine_.OverwriteServerCache(Path("zzz/yyy/xxx"), 300); + + engine_.MergeIntoServerCache( + Path("aaa/bbb"), std::map{{"ccc", 400}, {"eee", 500}}); + + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ccc")), 400); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ddd")), 200); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/eee")), 500); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb")), + Variant(std::map{ + {"ccc", 400}, {"ddd", 200}, {"eee", 500}})); + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 400}, + {"ddd", 200}, + {"eee", 500} + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }})); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, + MergeIntoServerCache_CompoundWrite) { + EXPECT_DEATH(engine_.MergeIntoServerCache(Path(), CompoundWrite()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, + MergeIntoServerCache_CompoundWrite) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaa/bbb/ccc"), 100); + engine_.OverwriteServerCache(Path("aaa/bbb/ddd"), 200); + engine_.OverwriteServerCache(Path("zzz/yyy/xxx"), 300); + + CompoundWrite write; + write = write.AddWrite(Path("ccc"), 400); + write = write.AddWrite(Path("eee"), 500); + + engine_.MergeIntoServerCache(Path("aaa/bbb"), write); + + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ccc")), 400); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ddd")), 200); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/eee")), 500); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb")), + Variant(std::map{ + {"ccc", 400}, {"ddd", 200}, {"eee", 500}})); + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 400}, + {"ddd", 200}, + {"eee", 500} + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }})); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineTest, ServerCacheEstimatedSizeInBytes) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaaa/bbbb"), + Variant::FromMutableString("abcdefghijklm")); + engine_.OverwriteServerCache(Path("aaaa/cccc"), + Variant::FromMutableString("nopqrstuvwxyz")); + engine_.OverwriteServerCache(Path("aaaa/dddd"), Variant::FromInt64(12345)); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + const int kKeyLengths = 4; // The keys used above are 4 characters; + const int kValueLengths = 13; // The values used above are 13 characters; + EXPECT_EQ(engine_.ServerCacheEstimatedSizeInBytes(), + 9 * sizeof(Variant) + 4 * kKeyLengths + 2 * kValueLengths); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveTrackedQuery) { + // Must be in a transaction. + EXPECT_DEATH(engine_.SaveTrackedQuery(TrackedQuery()), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, SaveTrackedQuery) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.SaveTrackedQuery(TrackedQuery()); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, DeleteTrackedQuery) { + // Must be in a transaction. + EXPECT_DEATH(engine_.DeleteTrackedQuery(100), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, DeleteTrackedQuery) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.DeleteTrackedQuery(100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, LoadTrackedQueries) { + // This is all in-memory, so nothing to read from disk. + EXPECT_TRUE(engine_.LoadTrackedQueries().empty()); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, PruneCache) { + engine_.BeginTransaction(); + // clang-format off + engine_.OverwriteServerCache( + Path(), + std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 100}, + {"ddd", 200} + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }}); + // clang-format on + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + PruneForest forest; + PruneForestRef ref(&forest); + + ref.Prune(Path("aaa/bbb")); + ref.Keep(Path("aaa/bbb/ccc")); + ref.Prune(Path("zzz")); + + engine_.PruneCache(Path(), ref); + + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 100} + }} + }} + })) << util::VariantToJson(engine_.ServerCache(Path())); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, + ResetPreviouslyActiveTrackedQueries) { + // Must be in a transaction. + EXPECT_DEATH(engine_.ResetPreviouslyActiveTrackedQueries(100), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, + ResetPreviouslyActiveTrackedQueries) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.ResetPreviouslyActiveTrackedQueries(100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveTrackedQueryKeys) { + // Must be in a transaction. + EXPECT_DEATH(engine_.SaveTrackedQueryKeys(100, std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, UpdateTrackedQueryKeys) { + EXPECT_DEATH(engine_.UpdateTrackedQueryKeys(100, std::set(), + std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, TrackedQueryKeys) { + engine_.BeginTransaction(); + + EXPECT_TRUE(engine_.LoadTrackedQueryKeys(100).empty()); + + engine_.SaveTrackedQueryKeys(100, {"aaa", "bbb", "ccc"}); + engine_.SaveTrackedQueryKeys(200, {"zzz", "yyy", "xxx"}); + + EXPECT_EQ(engine_.LoadTrackedQueryKeys(100), + std::set({"aaa", "bbb", "ccc"})); + EXPECT_EQ(engine_.LoadTrackedQueryKeys(200), + std::set({"zzz", "yyy", "xxx"})); + EXPECT_TRUE(engine_.LoadTrackedQueryKeys(300).empty()); + + engine_.UpdateTrackedQueryKeys(100, std::set({"ddd", "eee"}), + std::set({"aaa", "bbb"})); + + EXPECT_EQ(engine_.LoadTrackedQueryKeys(100), + std::set({"ccc", "ddd", "eee"})); + EXPECT_EQ(engine_.LoadTrackedQueryKeys(200), + std::set({"zzz", "yyy", "xxx"})); + EXPECT_TRUE(engine_.LoadTrackedQueryKeys(300).empty()); + + EXPECT_EQ(engine_.LoadTrackedQueryKeys(std::set({100})), + std::set({"ccc", "ddd", "eee"})); + EXPECT_EQ(engine_.LoadTrackedQueryKeys(std::set({100, 200})), + std::set({"ccc", "ddd", "eee", "zzz", "yyy", "xxx"})); + + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, BeginTransaction) { + EXPECT_TRUE(engine_.BeginTransaction()); + // Cannot begin a transaction while in a transaction. + EXPECT_DEATH(engine_.BeginTransaction(), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, BeginTransaction) { + // BeginTransaction should return true, indicating success. + EXPECT_TRUE(engine_.BeginTransaction()); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, EndTransaction) { + // Cannot end a transaction unless in a transaction. + EXPECT_DEATH(engine_.EndTransaction(), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, EndTransaction) { + EXPECT_TRUE(engine_.BeginTransaction()); + engine_.EndTransaction(); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc b/database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc new file mode 100644 index 0000000000..4bcfbc974e --- /dev/null +++ b/database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc @@ -0,0 +1,700 @@ +// Copyright 2020 Google LLC +// +// 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 "database/src/desktop/persistence/level_db_persistence_storage_engine.h" + +#include +#include +#include + +#include "app/src/logger.h" +#include "app/src/variant_util.h" +#include "database/src/desktop/persistence/flatbuffer_conversions.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// TODO(amablue): Consider refactoring this into a common location. +#if defined(_WIN32) +static const char kDirectorySeparator[] = "\\"; +#else +static const char kDirectorySeparator[] = "/"; +#endif // defined(_WIN32) + +static std::string GetTestTmpDir(const char test_namespace[]) { +#if defined(_WIN32) + char buf[MAX_PATH + 1]; + if (GetEnvironmentVariableA("TEST_TMPDIR", buf, sizeof(buf))) { + return std::string(buf) + kDirectorySeparator + test_namespace; + } +#else + // Linux and OS X should either have the TEST_TMPDIR environment variable set. + if (const char* value = getenv("TEST_TMPDIR")) { + return std::string(value) + kDirectorySeparator + test_namespace; + } +#endif // defined(_WIN32) + // If we weren't able to get TEST_TMPDIR, just use a subdirectory. + return test_namespace; +} + +TEST(LevelDbPersistenceStorageEngine, ConstructorBasic) { + const std::string kDatabaseFilename = GetTestTmpDir(test_info_->name()); + + // Just ensure that nothing crashes. + SystemLogger logger; + LevelDbPersistenceStorageEngine engine(&logger); + engine.Initialize(kDatabaseFilename); +} + +class LevelDbPersistenceStorageEngineTest : public ::testing::Test { + protected: + void SetUp() override { + engine_ = new LevelDbPersistenceStorageEngine(&logger_); + } + + void TearDown() override { delete engine_; } + + // All tests should start with this. This sets the path Level DB should read + // from and write to, and caches that path so that when we re-start Level DB + // we have the path we used on the previous run. + void InitializeLevelDb(const std::string& test_name) { + database_path_ = GetTestTmpDir(test_name.c_str()); + engine_->Initialize(database_path_); + } + + // We want to run all of our tests twice: Once immediately after the functions + // have been called on the database, and then once again after the database + // has been shut down and restarted. + template + void RunTwice(const Func& func) { + func(); + TearDown(); + SetUp(); + engine_->Initialize(database_path_); + func(); + } + + SystemLogger logger_; + LevelDbPersistenceStorageEngine* engine_; + std::string database_path_; +}; + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveUserOverwrite) { + InitializeLevelDb(test_info_->name()); + + Path path_a("aaa/bbb"); + Variant data_a("variant_data"); + WriteId write_id_a = 100; + + Path path_b("ccc/ddd"); + Variant data_b("variant_data_two"); + WriteId write_id_b = 101; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserOverwrite(path_a, data_a, write_id_a); + engine_->SaveUserOverwrite(path_b, data_b, write_id_b); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{ + UserWriteRecord(100, Path("aaa/bbb"), "variant_data", true), + UserWriteRecord(101, Path("ccc/ddd"), "variant_data_two", true)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveUserMerge) { + InitializeLevelDb(test_info_->name()); + + Path path("this/is/a/test/path"); + CompoundWrite children = CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("larry"), 999), + std::make_pair(Path("curly"), 888), + std::make_pair(Path("moe"), 777), + }); + WriteId write_id = 100; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserMerge(path, children, write_id); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{ + UserWriteRecord(100, Path("this/is/a/test/path"), + CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("larry"), 999), + std::make_pair(Path("curly"), 888), + std::make_pair(Path("moe"), 777), + }))}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, RemoveUserWrite) { + InitializeLevelDb(test_info_->name()); + + Path path_a("this/is/a/test/path"); + Variant data_a("variant_data"); + WriteId write_id_a = 100; + + Path path_b("this/is/another/test/path"); + Variant data_b("variant_data_two"); + WriteId write_id_b = 101; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserOverwrite(path_a, data_a, write_id_a); + engine_->SaveUserOverwrite(path_b, data_b, write_id_b); + engine_->RemoveUserWrite(100); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{ + UserWriteRecord(101, Path("this/is/another/test/path"), + Variant("variant_data_two"), true)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, RemoveAllUserWrites) { + InitializeLevelDb(test_info_->name()); + + Path path_a("this/is/a/test/path"); + Variant data_a("variant_data"); + WriteId write_id_a = 100; + + Path path_b("this/is/another/test/path"); + Variant data_b("variant_data_two"); + WriteId write_id_b = 101; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserOverwrite(path_a, data_a, write_id_a); + engine_->SaveUserOverwrite(path_b, data_b, write_id_b); + engine_->RemoveAllUserWrites(); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, OverwriteServerCache) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path("aaa/bbb"), Variant("some value")); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + Variant expected("some value"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa")); + // clang-format off + Variant expected = std::map{ + std::make_pair("bbb", "some value"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("aaa", std::map{ + std::make_pair("bbb", "some value"), + }) + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, OverwriteServerCache_Overwrite) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path("aaa/bbb"), Variant("some value")); + engine_->OverwriteServerCache(Path("aaa"), Variant("Overwrite!")); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + Variant expected = Variant::Null(); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa")); + Variant expected("Overwrite!"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("aaa", "Overwrite!"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, MergeIntoServerCacheWithVariant) { + InitializeLevelDb(test_info_->name()); + + Variant merge = std::map{ + std::make_pair("ccc", std::map{std::make_pair( + "ddd", "some value")}), + std::make_pair("eee", "adjacent value"), + }; + + engine_->BeginTransaction(); + engine_->MergeIntoServerCache(Path("aaa/bbb"), merge); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb/ccc/ddd")); + Variant expected = "some value"; + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb/eee")); + Variant expected("adjacent value"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + // clang-format off + Variant expected = std::map{ + std::make_pair("ccc", std::map{ + std::make_pair("ddd", "some value"), + }), + std::make_pair("eee", "adjacent value"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, + MergeIntoServerCacheWithCompoundWrite) { + InitializeLevelDb(test_info_->name()); + + CompoundWrite merge = CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("ccc/ddd"), "some value"), + std::make_pair(Path("eee"), "adjacent value"), + }); + + engine_->BeginTransaction(); + engine_->MergeIntoServerCache(Path("aaa/bbb"), merge); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb/ccc/ddd")); + Variant expected = "some value"; + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb/eee")); + Variant expected("adjacent value"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + // clang-format off + Variant expected = std::map{ + std::make_pair("ccc", std::map{ + std::make_pair("ddd", "some value"), + }), + std::make_pair("eee", "adjacent value"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("aaa", std::map{ + std::make_pair("bbb", std::map{ + std::make_pair("ccc", std::map{ + std::make_pair("ddd", "some value"), + }), + std::make_pair("eee", "adjacent value"), + }), + }), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, ServerCacheEstimatedSizeInBytes) { + InitializeLevelDb(test_info_->name()); + + std::string long_string(1024, 'x'); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path("aaa"), long_string); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + uint64 result = engine_->ServerCacheEstimatedSizeInBytes(); + uint64 expected = 1024 + strlen("aaa"); + + // This is only an estimate, so as long as we're within a few bytes it's + // okay. + EXPECT_NEAR(result, expected, 16); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveTrackedQuery) { + InitializeLevelDb(test_info_->name()); + + TrackedQuery tracked_query_a(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive); + TrackedQuery tracked_query_b(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + + engine_->BeginTransaction(); + engine_->SaveTrackedQuery(tracked_query_a); + engine_->SaveTrackedQuery(tracked_query_b); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadTrackedQueries(); + std::vector expected{ + TrackedQuery(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive), + TrackedQuery(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, TrackedQuery::kInactive)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, DeleteTrackedQuery) { + InitializeLevelDb(test_info_->name()); + + TrackedQuery tracked_query_a(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive); + TrackedQuery tracked_query_b(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + + engine_->BeginTransaction(); + engine_->SaveTrackedQuery(tracked_query_a); + engine_->SaveTrackedQuery(tracked_query_b); + engine_->DeleteTrackedQuery(100); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadTrackedQueries(); + std::vector expected{ + TrackedQuery(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, TrackedQuery::kInactive)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, + ResetPreviouslyActiveTrackedQueries) { + InitializeLevelDb(test_info_->name()); + + TrackedQuery tracked_query_a(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive); + TrackedQuery tracked_query_b(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + + engine_->BeginTransaction(); + engine_->SaveTrackedQuery(tracked_query_a); + engine_->SaveTrackedQuery(tracked_query_b); + engine_->ResetPreviouslyActiveTrackedQueries(9999); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadTrackedQueries(); + std::vector expected{ + TrackedQuery(100, QuerySpec(Path("aaa/bbb/ccc")), 9999, + TrackedQuery::kComplete, TrackedQuery::kInactive), + TrackedQuery(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, TrackedQuery::kInactive)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->SaveTrackedQueryKeys(100, + std::set{"key1", "key2", "key3"}); + engine_->SaveTrackedQueryKeys(101, + std::set{"key4", "key5", "key6"}); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + std::set result = engine_->LoadTrackedQueryKeys(100); + std::set expected{"key1", "key2", "key3"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = engine_->LoadTrackedQueryKeys(101); + std::set expected{"key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = + engine_->LoadTrackedQueryKeys(std::set{100, 101}); + std::set expected{"key1", "key2", "key3", + "key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, UpdateTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->SaveTrackedQueryKeys(100, + std::set{"key1", "key2", "key3"}); + engine_->SaveTrackedQueryKeys(101, + std::set{"key4", "key5", "key6"}); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + std::set result = engine_->LoadTrackedQueryKeys(100); + std::set expected{"key1", "key2", "key3"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = engine_->LoadTrackedQueryKeys(101); + std::set expected{"key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = + engine_->LoadTrackedQueryKeys(std::set{100, 101}); + std::set expected{"key1", "key2", "key3", + "key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, PruneCache) { + InitializeLevelDb(test_info_->name()); + + // clang-format off + Variant initial_data = std::map{ + std::make_pair("the_root", std::map{ + std::make_pair("delete_me", std::map{ + std::make_pair("but_keep_me", 111), + std::make_pair("ill_be_gone", 222), + }), + std::make_pair("keep_me", std::map{ + std::make_pair("but_delete_me", 333), + std::make_pair("ill_be_here", 444), + }), + }), + }; + // clang-format on + + PruneForest prune_forest; + PruneForestRef prune_forest_ref(&prune_forest); + prune_forest_ref.Prune(Path("delete_me")); + prune_forest_ref.Keep(Path("delete_me/but_keep_me")); + prune_forest_ref.Prune(Path("keep_me/but_delete_me")); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path(), initial_data); + engine_->PruneCache(Path("the_root"), prune_forest_ref); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("the_root", std::map{ + std::make_pair("delete_me", std::map{ + std::make_pair("but_keep_me", 111), + }), + std::make_pair("keep_me", std::map{ + std::make_pair("ill_be_here", 444), + }), + }), + }; + // clang-format on + EXPECT_EQ(result, expected); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, BeginTransaction) { + // BeginTransaction should return true, indicating success. + EXPECT_TRUE(engine_->BeginTransaction()); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, EndTransaction) { + EXPECT_TRUE(engine_->BeginTransaction()); + engine_->EndTransaction(); +} + +// Many functions are designed to assert if called outside a transaction. Ensure +// they crash as expected. +using LevelDbPersistenceStorageEngineDeathTest = + LevelDbPersistenceStorageEngineTest; + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveUserOverwrite) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveUserOverwrite(Path(), Variant(), 0), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveUserMerge) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveUserMerge(Path(), CompoundWrite(), 0), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, RemoveUserWrite) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->RemoveUserWrite(0), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, RemoveAllUserWrites) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->RemoveAllUserWrites(), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, OverwriteServerCache) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->OverwriteServerCache(Path(), Variant()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, MergeIntoServerCacheVariant) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->MergeIntoServerCache(Path(), Variant()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, + MergeIntoServerCacheCompoundWrite) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->MergeIntoServerCache(Path(), CompoundWrite()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveTrackedQuery) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveTrackedQuery(TrackedQuery()), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, DeleteTrackedQuery) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->DeleteTrackedQuery(0), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, + ResetPreviouslyActiveTrackedQueries) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->ResetPreviouslyActiveTrackedQueries(0), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveTrackedQueryKeys(0, std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, UpdateTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->UpdateTrackedQueryKeys(0, std::set(), + std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, PruneCache) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->PruneCache(Path(), PruneForestRef(nullptr)), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, BeginTransaction) { + EXPECT_TRUE(engine_->BeginTransaction()); + // Cannot begin a transaction while in a transaction. + EXPECT_DEATH(engine_->BeginTransaction(), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, EndTransaction) { + // Cannot end a transaction unless in a transaction. + EXPECT_DEATH(engine_->EndTransaction(), DEATHTEST_SIGABRT); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/noop_persistence_manager_test.cc b/database/tests/desktop/persistence/noop_persistence_manager_test.cc new file mode 100644 index 0000000000..a60fcfa8c5 --- /dev/null +++ b/database/tests/desktop/persistence/noop_persistence_manager_test.cc @@ -0,0 +1,86 @@ +// Copyright 2020 Google LLC +// +// 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 "database/src/desktop/persistence/noop_persistence_manager.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(NoopPersistenceManager, Constructor) { + // Ensure there is no crash. + NoopPersistenceManager manager; + (void)manager; +} + +TEST(NoopPersistenceManager, LoadUserWrites) { + NoopPersistenceManager manager; + EXPECT_TRUE(manager.LoadUserWrites().empty()); +} + +TEST(NoopPersistenceManager, ServerCache) { + NoopPersistenceManager manager; + EXPECT_EQ(manager.ServerCache(QuerySpec()), CacheNode()); +} + +TEST(NoopPersistenceManager, InsideTransaction) { + // Make sure none of these functions result in a crash. There is no state we + // can query or other side effects that we can test. + NoopPersistenceManager manager; + EXPECT_TRUE(manager.RunInTransaction([&manager]() { + manager.SaveUserMerge(Path(), CompoundWrite(), 100); + manager.RemoveUserWrite(100); + manager.RemoveAllUserWrites(); + manager.ApplyUserWriteToServerCache(Path("a/b/c"), Variant::FromInt64(123)); + manager.ApplyUserWriteToServerCache(Path("a/b/c"), CompoundWrite()); + manager.UpdateServerCache(QuerySpec(), Variant::FromInt64(123)); + manager.UpdateServerCache(Path("a/b/c"), CompoundWrite()); + manager.SetQueryActive(QuerySpec()); + manager.SetQueryInactive(QuerySpec()); + manager.SetQueryComplete(QuerySpec()); + manager.SetTrackedQueryKeys(QuerySpec(), + std::set{"aaa", "bbb"}); + manager.UpdateTrackedQueryKeys(QuerySpec(), + std::set{"aaa", "bbb"}, + std::set{"ccc", "ddd"}); + return true; + })); +} + +TEST(NoopPersistenceManagerDeathTest, NestedTransaction) { + // Make sure none of these functions result in a crash. There is no state we + // can query or other side effects that we can test. + NoopPersistenceManager manager; + EXPECT_DEATH(manager.RunInTransaction([&manager]() { + // This transaction should run. + manager.RunInTransaction([]() { + // This transaction should not run, since the nested call to + // RunInTransaction should assert. + return true; + }); + return true; + }), + DEATHTEST_SIGABRT); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/persistence_manager_test.cc b/database/tests/desktop/persistence/persistence_manager_test.cc new file mode 100644 index 0000000000..99efc673b0 --- /dev/null +++ b/database/tests/desktop/persistence/persistence_manager_test.cc @@ -0,0 +1,461 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/persistence/persistence_manager.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/tests/desktop/test/mock_cache_policy.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_tracked_query_manager.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::StrictMock; +using testing::Test; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +class PersistenceManagerTest : public Test { + public: + void SetUp() override { + storage_engine_ = new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine_); + + tracked_query_manager_ = new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager_); + + cache_policy_ = new NiceMock(); + UniquePtr cache_policy_ptr(cache_policy_); + + manager_ = new PersistenceManager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger_); + } + + void TearDown() override { delete manager_; } + + protected: + MockPersistenceStorageEngine* storage_engine_; + MockTrackedQueryManager* tracked_query_manager_; + MockCachePolicy* cache_policy_; + SystemLogger logger_; + + PersistenceManager* manager_; +}; + +TEST_F(PersistenceManagerTest, SaveUserOverwrite) { + EXPECT_CALL( + *storage_engine_, + SaveUserOverwrite(Path("test/path"), Variant("test_variant"), 100)); + + manager_->SaveUserOverwrite(Path("test/path"), Variant("test_variant"), 100); +} + +TEST_F(PersistenceManagerTest, SaveUserMerge) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_CALL(*storage_engine_, SaveUserMerge(Path("test/path"), write, 100)); + + manager_->SaveUserMerge(Path("test/path"), write, 100); +} + +TEST_F(PersistenceManagerTest, RemoveUserWrite) { + EXPECT_CALL(*storage_engine_, RemoveUserWrite(100)); + + manager_->RemoveUserWrite(100); +} + +TEST_F(PersistenceManagerTest, RemoveAllUserWrites) { + EXPECT_CALL(*storage_engine_, RemoveAllUserWrites()); + + manager_->RemoveAllUserWrites(); +} + +TEST_F(PersistenceManagerTest, ApplyUserWriteToServerCacheWithoutActiveQuery) { + // If there is no active default query, we expect it to apply the variant to + // the storage engine at the given path. + EXPECT_CALL(*tracked_query_manager_, HasActiveDefaultQuery(Path("abc"))) + .WillOnce(Return(false)); + EXPECT_CALL(*storage_engine_, + OverwriteServerCache(Path("abc"), Variant("zyx"))); + EXPECT_CALL(*tracked_query_manager_, EnsureCompleteTrackedQuery(Path("abc"))); + + manager_->ApplyUserWriteToServerCache(Path("abc"), "zyx"); +} + +TEST_F(PersistenceManagerTest, ApplyUserWriteToServerCacheWithActiveQuery) { + // If there is an active default query, nothing should happen. + EXPECT_CALL(*tracked_query_manager_, HasActiveDefaultQuery(Path("abc"))) + .WillOnce(Return(true)); + + manager_->ApplyUserWriteToServerCache(Path("abc"), Variant("zyx")); +} + +TEST_F(PersistenceManagerTest, ApplyUserWriteToServerCacheWithCompoundWrite) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_CALL(*tracked_query_manager_, HasActiveDefaultQuery(_)) + .WillRepeatedly(Return(false)); + + EXPECT_CALL(*storage_engine_, OverwriteServerCache(Path("aaa"), Variant(1))); + EXPECT_CALL(*tracked_query_manager_, EnsureCompleteTrackedQuery(Path("aaa"))); + + EXPECT_CALL(*storage_engine_, OverwriteServerCache(Path("bbb"), Variant(2))); + EXPECT_CALL(*tracked_query_manager_, EnsureCompleteTrackedQuery(Path("bbb"))); + + EXPECT_CALL(*storage_engine_, + OverwriteServerCache(Path("ccc/ddd"), Variant(3))); + EXPECT_CALL(*tracked_query_manager_, + EnsureCompleteTrackedQuery(Path("ccc/ddd"))); + + EXPECT_CALL(*storage_engine_, + OverwriteServerCache(Path("ccc/eee"), Variant(4))); + EXPECT_CALL(*tracked_query_manager_, + EnsureCompleteTrackedQuery(Path("ccc/eee"))); + + manager_->ApplyUserWriteToServerCache(Path(), write); +} + +TEST_F(PersistenceManagerTest, LoadUserWrites) { + EXPECT_CALL(*storage_engine_, LoadUserWrites()); + manager_->LoadUserWrites(); +} + +TEST_F(PersistenceManagerTest, ServerCache_QueryComplete) { + QuerySpec query_spec; + query_spec.params.start_at_value = "zzz"; + query_spec.path = Path("abc"); + + TrackedQuery tracked_query; + tracked_query.query_id = 1234; + tracked_query.active = true; + tracked_query.complete = true; + + std::set tracked_keys{"aaa", "ccc"}; + + Variant server_cache(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }); + + EXPECT_CALL(*tracked_query_manager_, IsQueryComplete(query_spec)) + .WillOnce(Return(true)); + EXPECT_CALL(*tracked_query_manager_, FindTrackedQuery(query_spec)) + .WillOnce(Return(&tracked_query)); + EXPECT_CALL(*storage_engine_, LoadTrackedQueryKeys(1234)) + .WillOnce(Return(tracked_keys)); + EXPECT_CALL(*storage_engine_, ServerCache(Path("abc"))) + .WillOnce(Return(server_cache)); + + CacheNode result = manager_->ServerCache(query_spec); + CacheNode expected_result( + IndexedVariant(Variant(std::map{ + std::make_pair("aaa", 1), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }), + query_spec.params), + true, true); + + EXPECT_EQ(result, expected_result); +} + +TEST_F(PersistenceManagerTest, ServerCache_QueryIncomplete) { + QuerySpec query_spec; + query_spec.params.start_at_value = "zzz"; + query_spec.path = Path("abc"); + + TrackedQuery tracked_query; + tracked_query.query_id = 1234; + tracked_query.active = true; + tracked_query.complete = false; + + std::set tracked_keys{"aaa", "ccc"}; + + Variant server_cache(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }); + + EXPECT_CALL(*tracked_query_manager_, IsQueryComplete(query_spec)) + .WillOnce(Return(false)); + EXPECT_CALL(*tracked_query_manager_, GetKnownCompleteChildren(Path("abc"))) + .WillOnce(Return(tracked_keys)); + EXPECT_CALL(*storage_engine_, ServerCache(Path("abc"))) + .WillOnce(Return(server_cache)); + + CacheNode result = manager_->ServerCache(query_spec); + CacheNode expected_result( + IndexedVariant(Variant(std::map{ + std::make_pair("aaa", 1), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }), + query_spec.params), + false, true); + + EXPECT_EQ(result, expected_result); +} + +TEST_F(PersistenceManagerTest, UpdateServerCache_LoadsAllData) { + Path path; + Variant variant; + QuerySpec query_spec; + query_spec.path = path; + + EXPECT_CALL(*storage_engine_, OverwriteServerCache(path, variant)); + + manager_->UpdateServerCache(query_spec, variant); +} + +TEST_F(PersistenceManagerTest, UpdateServerCache_DoesntLoadAllData) { + Path path; + Variant variant; + QuerySpec query_spec; + query_spec.params.start_at_value = "bbb"; + query_spec.path = path; + + EXPECT_CALL(*storage_engine_, MergeIntoServerCache(path, variant)); + + manager_->UpdateServerCache(query_spec, variant); +} + +TEST_F(PersistenceManagerTest, UpdateServerCache_WithCompoundWrite) { + Path path; + + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_CALL(*storage_engine_, MergeIntoServerCache(path, write)); + + manager_->UpdateServerCache(path, write); +} + +TEST_F(PersistenceManagerTest, SetQueryActive) { + EXPECT_CALL(*tracked_query_manager_, + SetQueryActiveFlag(QuerySpec(), TrackedQuery::kActive)); + + manager_->SetQueryActive(QuerySpec()); +} + +TEST_F(PersistenceManagerTest, SetQueryInactive) { + EXPECT_CALL(*tracked_query_manager_, + SetQueryActiveFlag(QuerySpec(), TrackedQuery::kInactive)); + + manager_->SetQueryInactive(QuerySpec()); +} + +TEST_F(PersistenceManagerTest, SetQueryComplete) { + QuerySpec loads_all_data; + loads_all_data.path = Path("aaa"); + QuerySpec does_not_load_all_data; + does_not_load_all_data.path = Path("bbb"); + does_not_load_all_data.params.start_at_value = "abc"; + + EXPECT_CALL(*tracked_query_manager_, SetQueriesComplete(Path("aaa"))); + manager_->SetQueryComplete(loads_all_data); + + EXPECT_CALL(*tracked_query_manager_, + SetQueryCompleteIfExists(does_not_load_all_data)); + manager_->SetQueryComplete(does_not_load_all_data); +} + +TEST_F(PersistenceManagerTest, SetTrackedQueryKeys) { + QuerySpec query_spec; + query_spec.params.start_at_value = "baa"; + std::set keys{"foo", "bar", "baz"}; + + TrackedQuery tracked_query; + tracked_query.query_id = 1234; + tracked_query.active = true; + EXPECT_CALL(*tracked_query_manager_, FindTrackedQuery(query_spec)) + .WillOnce(Return(&tracked_query)); + EXPECT_CALL(*storage_engine_, SaveTrackedQueryKeys(1234, keys)); + + manager_->SetTrackedQueryKeys(query_spec, keys); +} + +TEST_F(PersistenceManagerTest, UpdateTrackedQueryKeys) { + QuerySpec query_spec; + query_spec.params.start_at_value = "baa"; + std::set added{"foo", "bar", "baz"}; + std::set removed{"qux", "quux", "quuz"}; + + TrackedQuery tracked_query; + tracked_query.query_id = 9876; + tracked_query.active = true; + EXPECT_CALL(*tracked_query_manager_, FindTrackedQuery(query_spec)) + .WillOnce(Return(&tracked_query)); + EXPECT_CALL(*storage_engine_, UpdateTrackedQueryKeys(9876, added, removed)); + + manager_->UpdateTrackedQueryKeys(query_spec, added, removed); +} + +TEST(PersistenceManager, DoPruneCheckAfterServerUpdate_DoNotCheckCacheSize) { + MockPersistenceStorageEngine* storage_engine = + new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine); + + MockTrackedQueryManager* tracked_query_manager = + new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager); + + MockCachePolicy* cache_policy = new StrictMock(); + UniquePtr cache_policy_ptr(cache_policy); + + SystemLogger logger; + PersistenceManager manager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger); + + // After the server cache is updated, DoPruneCheckAfterServerUpdate will be + // called. It should call CachePolicy::ShouldCheckCacheSize once, and if it + // returns false, it should not do anything else. + EXPECT_CALL(*cache_policy, ShouldCheckCacheSize(_)).WillOnce(Return(false)); + + manager.UpdateServerCache(QuerySpec(), Variant()); +} + +TEST(PersistenceManager, DoPruneCheckAfterServerUpdate_DoCheckCacheSize) { + MockPersistenceStorageEngine* storage_engine = + new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine); + + MockTrackedQueryManager* tracked_query_manager = + new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager); + + MockCachePolicy* cache_policy = new StrictMock(); + UniquePtr cache_policy_ptr(cache_policy); + + SystemLogger logger; + PersistenceManager manager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger); + + // After the server cache is updated, DoPruneCheckAfterServerUpdate will be + // called. It should call CachePolicy::ShouldCheckCacheSize once, and if it + // returns true, it will then check if it should prune anything. If + // CachePolicy::ShouldPrune returns false, nothing else will happen. + EXPECT_CALL(*cache_policy, ShouldCheckCacheSize(_)).WillOnce(Return(true)); + EXPECT_CALL(*cache_policy, ShouldPrune(_, _)).WillOnce(Return(false)); + + manager.UpdateServerCache(QuerySpec(), Variant()); +} + +TEST(PersistenceManager, DoPruneCheckAfterServerUpdate_PruneStuff) { + MockPersistenceStorageEngine* storage_engine = + new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine); + + MockTrackedQueryManager* tracked_query_manager = + new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager); + + MockCachePolicy* cache_policy = new StrictMock(); + UniquePtr cache_policy_ptr(cache_policy); + + SystemLogger logger; + PersistenceManager manager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger); + + // After the server cache is updated, DoPruneCheckAfterServerUpdate will be + // called. It should call CachePolicy::ShouldCheckCacheSize once, and if it + // returns true, it will then check if it should prune anything. If + // CachePolicy::ShouldPrune returns true, it will pass the prune tree to + // StorageEngine::PruneCache. + EXPECT_CALL(*cache_policy, ShouldCheckCacheSize(_)).WillOnce(Return(true)); + EXPECT_CALL(*cache_policy, ShouldPrune(_, _)) + .WillOnce(Return(true)) + .WillOnce(Return(false)); + PruneForest prune_forest; + prune_forest.set_value(true); + EXPECT_CALL(*tracked_query_manager, PruneOldQueries(_)) + .WillOnce(Return(prune_forest)); + EXPECT_CALL(*storage_engine, + PruneCache(Path(), PruneForestRef(&prune_forest))); + + manager.UpdateServerCache(QuerySpec(), Variant()); +} + +TEST_F(PersistenceManagerTest, RunInTransaction_StdFunctionSuccess) { + EXPECT_CALL(*storage_engine_, BeginTransaction()); + EXPECT_CALL(*storage_engine_, SetTransactionSuccessful()); + EXPECT_CALL(*storage_engine_, EndTransaction()); + bool function_called = false; + EXPECT_TRUE(manager_->RunInTransaction([&]() { + function_called = true; + return true; + })); + EXPECT_TRUE(function_called); +} + +TEST_F(PersistenceManagerTest, RunInTransaction_StdFunctionFailure) { + EXPECT_CALL(*storage_engine_, BeginTransaction()); + EXPECT_CALL(*storage_engine_, EndTransaction()); + bool function_called = false; + EXPECT_FALSE(manager_->RunInTransaction([&]() { + function_called = true; + return false; + })); + EXPECT_TRUE(function_called); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/prune_forest_test.cc b/database/tests/desktop/persistence/prune_forest_test.cc new file mode 100644 index 0000000000..efdf524d94 --- /dev/null +++ b/database/tests/desktop/persistence/prune_forest_test.cc @@ -0,0 +1,485 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/persistence/prune_forest.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(PruneForestTest, Equality) { + PruneForest forest; + forest.SetValueAt(Path("true"), true); + forest.SetValueAt(Path("false"), false); + + PruneForest identical_forest; + identical_forest.SetValueAt(Path("true"), true); + identical_forest.SetValueAt(Path("false"), false); + + PruneForest different_forest; + different_forest.SetValueAt(Path("true"), false); + different_forest.SetValueAt(Path("false"), true); + + PruneForestRef ref(&forest); + PruneForestRef same_ref(&forest); + PruneForestRef identical_ref(&identical_forest); + PruneForestRef different_ref(&different_forest); + PruneForestRef null_ref(nullptr); + PruneForestRef another_null_ref(nullptr); + + EXPECT_EQ(ref, ref); + EXPECT_EQ(ref, same_ref); + EXPECT_EQ(ref, identical_ref); + EXPECT_NE(ref, different_ref); + EXPECT_NE(ref, null_ref); + + EXPECT_EQ(null_ref, null_ref); + EXPECT_EQ(null_ref, another_null_ref); + EXPECT_EQ(another_null_ref, null_ref); +} + +TEST(PruneForestTest, PrunesAnything) { + { + PruneForest forest; + PruneForestRef ref(&forest); + EXPECT_FALSE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo")); + EXPECT_TRUE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo/bar/baz")); + EXPECT_TRUE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo")); + EXPECT_FALSE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo/bar/baz")); + EXPECT_FALSE(ref.PrunesAnything()); + } +} + +TEST(PruneForestTest, ShouldPruneUnkeptDescendants) { + { + PruneForest forest; + PruneForestRef ref(&forest); + + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path())); + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("aaa"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("bbb"), false); + + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path())); + EXPECT_TRUE(ref.ShouldPruneUnkeptDescendants(Path("aaa"))); + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("bbb"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), false); + forest.SetValueAt(Path("aaa/bbb"), true); + forest.SetValueAt(Path("aaa/bbb/ccc"), false); + + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("aaa"))); + EXPECT_TRUE(ref.ShouldPruneUnkeptDescendants(Path("aaa/bbb"))); + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("aaa/bbb/ccc"))); + } +} + +TEST(PruneForestTest, ShouldKeep) { + { + PruneForest forest; + PruneForestRef ref(&forest); + + EXPECT_FALSE(ref.ShouldKeep(Path())); + EXPECT_FALSE(ref.ShouldKeep(Path("aaa"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("bbb"), false); + + EXPECT_FALSE(ref.ShouldKeep(Path())); + EXPECT_FALSE(ref.ShouldKeep(Path("aaa"))); + EXPECT_TRUE(ref.ShouldKeep(Path("bbb"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("aaa/bbb"), false); + + EXPECT_FALSE(ref.ShouldKeep(Path("aaa"))); + EXPECT_TRUE(ref.ShouldKeep(Path("aaa/bbb"))); + } +} + +TEST(PruneForestTest, AffectsPath) { + { + PruneForest forest; + PruneForestRef ref(&forest); + + EXPECT_FALSE(ref.AffectsPath(Path())); + EXPECT_FALSE(ref.AffectsPath(Path("foo"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo")); + + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo/bar/baz")); + + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo")); + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo/bar/baz")); + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo")); + ref.Keep(Path("foo/bar/baz")); + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } +} + +TEST(PruneForestTest, GetChild) { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("aaa/bbb"), true); + forest.SetValueAt(Path("aaa/bbb/ccc"), true); + forest.SetValueAt(Path("zzz"), false); + forest.SetValueAt(Path("zzz/yyy"), false); + forest.SetValueAt(Path("zzz/yyy/xxx"), false); + + PruneForest* child_aaa = forest.GetChild(Path("aaa")); + PruneForest* child_aaa_bbb = forest.GetChild(Path("aaa/bbb")); + PruneForest* child_aaa_bbb_ccc = forest.GetChild(Path("aaa/bbb/ccc")); + PruneForest* child_zzz = forest.GetChild(Path("zzz")); + PruneForest* child_zzz_yyy = forest.GetChild(Path("zzz/yyy")); + PruneForest* child_zzz_yyy_xxx = forest.GetChild(Path("zzz/yyy/xxx")); + + EXPECT_EQ(ref.GetChild(Path("aaa")), PruneForestRef(child_aaa)); + EXPECT_EQ(ref.GetChild(Path("aaa/bbb")), PruneForestRef(child_aaa_bbb)); + EXPECT_EQ(ref.GetChild(Path("aaa/bbb/ccc")), + PruneForestRef(child_aaa_bbb_ccc)); + EXPECT_EQ(ref.GetChild(Path("zzz")), PruneForestRef(child_zzz)); + EXPECT_EQ(ref.GetChild(Path("zzz/yyy")), PruneForestRef(child_zzz_yyy)); + EXPECT_EQ(ref.GetChild(Path("zzz/yyy/xxx")), + PruneForestRef(child_zzz_yyy_xxx)); +} + +TEST(PruneForestTest, Prune) { + PruneForest forest; + PruneForestRef ref(&forest); + + ref.Prune(Path("aaa/bbb/ccc")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/bbb/ccc"))); + + ref.Prune(Path("aaa/bbb")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/bbb"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Prune(Path("aaa")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Prune(Path()); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Prune(Path("zzz")); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + + ref.Prune(Path("zzz/yyy")); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + + ref.Prune(Path("zzz/yyy/xxx")); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy/xxx")), nullptr); +} + +TEST(PruneForestTest, Keep) { + PruneForest forest; + PruneForestRef ref(&forest); + + ref.Keep(Path("aaa/bbb/ccc")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/bbb/ccc"))); + + ref.Keep(Path("aaa/bbb")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/bbb"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Keep(Path("aaa")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Keep(Path()); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Keep(Path("zzz")); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + + ref.Keep(Path("zzz/yyy")); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + + ref.Keep(Path("zzz/yyy/xxx")); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy/xxx")), nullptr); +} + +TEST(PruneForestTest, KeepAll) { + // Set up a test case. + PruneForest default_forest; + default_forest.SetValueAt(Path("aaa/111"), true); + default_forest.SetValueAt(Path("aaa/222"), false); + + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path("aaa"), std::set({std::string("111")})); + + // Only 111 should be affected, and it should now be false. + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path("aaa"), std::set({std::string("222")})); + + // Only 222 should be affected, but it was already false so it should not + // change. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path("aaa"), std::set( + {std::string("111"), std::string("222")})); + + // Both children should now be false. + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path(), std::set({std::string("aaa")})); + + // aaa should now be false, and all children of it should be eliminated. + EXPECT_FALSE(*forest.GetValueAt(Path("aaa"))); + + // Children are now eliminated. + EXPECT_EQ(forest.GetValueAt(Path("aaa/111")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/222")), nullptr); + } +} + +TEST(PruneForestTest, PruneAll) { + // Set up a test case. + PruneForest default_forest; + default_forest.SetValueAt(Path("aaa/111"), true); + default_forest.SetValueAt(Path("aaa/222"), false); + default_forest.SetValueAt(Path("bbb/111"), true); + default_forest.SetValueAt(Path("bbb/222"), false); + + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path("aaa"), std::set({std::string("111")})); + + // Only 111 should be affected, but it was already true so it should not + // change. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path("aaa"), std::set({std::string("222")})); + + // Only 222 should be affected, and it should now be true + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/222"))); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path("aaa"), std::set( + {std::string("111"), std::string("222")})); + + // Both children should now be true. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/222"))); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path(), std::set({std::string("aaa")})); + + // aaa should now be true, and all children of it should be eliminated. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa"))); + + // Children are now eliminated. + EXPECT_EQ(forest.GetValueAt(Path("aaa/111")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/222")), nullptr); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/push_child_name_generator_test.cc b/database/tests/desktop/push_child_name_generator_test.cc new file mode 100644 index 0000000000..737d08e12f --- /dev/null +++ b/database/tests/desktop/push_child_name_generator_test.cc @@ -0,0 +1,87 @@ +// Copyright 2017 Google LLC +// +// 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 "database/src/desktop/push_child_name_generator.h" + +#include +#include +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "thread/fiber/fiber.h" + +namespace { + +using ::firebase::database::internal::PushChildNameGenerator; +using ::testing::Eq; +using ::testing::Lt; + +TEST(PushChildNameGeneratorTest, TestOrderOfGeneratedNamesSameTime) { + PushChildNameGenerator generator; + + // Names should be generated in a way such that they are lexicographically + // increasing. + std::vector keys; + keys.reserve(100); + for (int i = 0; i < 100; ++i) { + keys.push_back(generator.GeneratePushChildName(0)); + } + for (int i = 0; i < 99; ++i) { + EXPECT_THAT(keys[i], Lt(keys[i + 1])); + } +} + +TEST(PushChildNameGeneratorTest, TestOrderOfGeneratedNamesDifferentTime) { + PushChildNameGenerator generator; + const int kNumToTest = 100; + + // Names should be generated in a way such that they are lexicographically + // increasing. + std::vector keys; + keys.reserve(kNumToTest); + for (int i = 0; i < kNumToTest; ++i) { + keys.push_back(generator.GeneratePushChildName(i)); + } + for (int i = 0; i < kNumToTest - 1; ++i) { + EXPECT_THAT(keys[i], Lt(keys[i + 1])); + } +} + +TEST(PushChildNameGeneratorTest, TestSimultaneousGeneratedNames) { + PushChildNameGenerator generator; + const int kNumToTest = 100; + + // Create a bunch of keys. + std::vector keys; + keys.resize(kNumToTest); + std::vector fibers; + for (int i = 0; i < kNumToTest; i++) { + fibers.push_back(new thread::Fiber([&generator, &keys, i]() { + keys[i] = generator.GeneratePushChildName(std::time(nullptr)); + })); + } + + // Insert keys into set. If there is a duplicate key, it will be discarded. + std::set key_set; + for (int i = 0; i < kNumToTest; i++) { + fibers[i]->Join(); + key_set.insert(keys[i]); + delete fibers[i]; + fibers[i] = nullptr; + } + + // Ensure that all keys are unique by making sure no keys were discarded. + EXPECT_THAT(key_set.size(), Eq(kNumToTest)); +} + +} // namespace diff --git a/database/tests/desktop/test/matchers.h b/database/tests/desktop/test/matchers.h new file mode 100644 index 0000000000..b6fc84fb82 --- /dev/null +++ b/database/tests/desktop/test/matchers.h @@ -0,0 +1,40 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MATCHERS_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MATCHERS_H_ + +#include +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +// Check a smart pointer with a raw pointer for equality. Ideally we would just +// do: +// +// Pointwise(Property(&UniquePtr::get, Eq())), +// +// but Property can't handle tuple matchers. +MATCHER(SmartPtrRawPtrEq, "CheckSmartPtrRawPtrEq") { + return std::get<0>(arg).get() == std::get<1>(arg); +} + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MATCHERS_H_ diff --git a/database/tests/desktop/test/matchers_test.cc b/database/tests/desktop/test/matchers_test.cc new file mode 100644 index 0000000000..1bc44bcf29 --- /dev/null +++ b/database/tests/desktop/test/matchers_test.cc @@ -0,0 +1,73 @@ +// Copyright 2018 Google LLC +// +// 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 "database/tests/desktop/test/matchers.h" + +#include + +#include "app/memory/unique_ptr.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Not; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { + +TEST(SmartPtrRawPtrEq, Matcher) { + int* five = new int(5); + EXPECT_THAT(std::make_tuple(UniquePtr(five), five), SmartPtrRawPtrEq()); + + int* ten = new int(10); + int* different_ten = new int(10); + EXPECT_THAT(std::make_tuple(UniquePtr(ten), different_ten), + Not(SmartPtrRawPtrEq())); + delete different_ten; +} + +TEST(SmartPtrRawPtrEq, Pointwise) { + int* five = new int(5); + int* ten = new int(10); + int* fifteen = new int(15); + int* twenty = new int(20); + int* different_twenty = new int(20); + std::vector> unique_values{ + UniquePtr(five), + UniquePtr(ten), + UniquePtr(fifteen), + UniquePtr(twenty), + }; + std::vector raw_values{ + five, + ten, + fifteen, + twenty, + }; + std::vector wrong_raw_values{ + five, + ten, + fifteen, + different_twenty, + }; + EXPECT_THAT(unique_values, Pointwise(SmartPtrRawPtrEq(), raw_values)); + EXPECT_THAT(unique_values, + Not(Pointwise(SmartPtrRawPtrEq(), wrong_raw_values))); + delete different_twenty; +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/test/mock_cache_policy.h b/database/tests/desktop/test/mock_cache_policy.h new file mode 100644 index 0000000000..ae61bb7413 --- /dev/null +++ b/database/tests/desktop/test/mock_cache_policy.h @@ -0,0 +1,45 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_CACHE_POLICY_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_CACHE_POLICY_H_ + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/cache_policy.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockCachePolicy : public CachePolicy { + public: + ~MockCachePolicy() override {} + + MOCK_METHOD(bool, ShouldPrune, + (uint64_t current_size_bytes, uint64_t count_of_prunable_queries), + (const, override)); + MOCK_METHOD(bool, ShouldCheckCacheSize, + (uint64_t server_updates_since_last_check), (const, override)); + MOCK_METHOD(double, GetPercentOfQueriesToPruneAtOnce, (), (const, override)); + MOCK_METHOD(uint64_t, GetMaxNumberOfQueriesToKeep, (), (const, override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_CACHE_POLICY_H_ diff --git a/database/tests/desktop/test/mock_listen_provider.h b/database/tests/desktop/test/mock_listen_provider.h new file mode 100644 index 0000000000..67d83ca796 --- /dev/null +++ b/database/tests/desktop/test/mock_listen_provider.h @@ -0,0 +1,40 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTEN_PROVIDER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTEN_PROVIDER_H_ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/listen_provider.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockListenProvider : public ListenProvider { + public: + MOCK_METHOD(void, StartListening, + (const QuerySpec& query_spec, const Tag& tag, const View* view), + (override)); + MOCK_METHOD(void, StopListening, + (const QuerySpec& query_spec, const Tag& tag), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTEN_PROVIDER_H_ diff --git a/database/tests/desktop/test/mock_listener.h b/database/tests/desktop/test/mock_listener.h new file mode 100644 index 0000000000..f612e9bcc8 --- /dev/null +++ b/database/tests/desktop/test/mock_listener.h @@ -0,0 +1,55 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTENER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTENER_H_ + +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/include/firebase/database/common.h" +#include "database/src/include/firebase/database/listener.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockValueListener : public ValueListener { + public: + MOCK_METHOD(void, OnValueChanged, (const DataSnapshot& snapshot), (override)); + MOCK_METHOD(void, OnCancelled, + (const Error& error, const char* error_message), (override)); +}; + +class MockChildListener : public ChildListener { + public: + MOCK_METHOD(void, OnChildAdded, + (const DataSnapshot& snapshot, const char* previous_sibling_key), + (override)); + MOCK_METHOD(void, OnChildChanged, + (const DataSnapshot& snapshot, const char* previous_sibling_key), + (override)); + MOCK_METHOD(void, OnChildMoved, + (const DataSnapshot& snapshot, const char* previous_sibling_key), + (override)); + MOCK_METHOD(void, OnChildRemoved, (const DataSnapshot& snapshot), (override)); + MOCK_METHOD(void, OnCancelled, + (const Error& error, const char* error_message), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTENER_H_ diff --git a/database/tests/desktop/test/mock_persistence_manager.h b/database/tests/desktop/test/mock_persistence_manager.h new file mode 100644 index 0000000000..774be6a488 --- /dev/null +++ b/database/tests/desktop/test/mock_persistence_manager.h @@ -0,0 +1,77 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_MANAGER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_MANAGER_H_ + +#include "app/src/include/firebase/variant.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/cache_policy.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/src/desktop/persistence/persistence_manager.h" +#include "database/src/desktop/view/view_cache.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockPersistenceManager : public PersistenceManager { + public: + MockPersistenceManager( + UniquePtr storage_engine, + UniquePtr tracked_query_manager, + UniquePtr cache_policy, LoggerBase* logger) + : PersistenceManager(std::move(storage_engine), + std::move(tracked_query_manager), + std::move(cache_policy), logger) {} + ~MockPersistenceManager() override {} + + MOCK_METHOD(void, SaveUserOverwrite, + (const Path& path, const Variant& variant, WriteId write_id), + (override)); + MOCK_METHOD(void, SaveUserMerge, + (const Path& path, const CompoundWrite& children, + WriteId write_id), + (override)); + MOCK_METHOD(void, RemoveUserWrite, (WriteId write_id), (override)); + MOCK_METHOD(void, RemoveAllUserWrites, (), (override)); + MOCK_METHOD(void, ApplyUserWriteToServerCache, + (const Path& path, const Variant& variant), (override)); + MOCK_METHOD(void, ApplyUserWriteToServerCache, + (const Path& path, const CompoundWrite& merge), (override)); + MOCK_METHOD(std::vector, LoadUserWrites, (), (override)); + MOCK_METHOD(CacheNode, ServerCache, (const QuerySpec& query), (override)); + MOCK_METHOD(void, UpdateServerCache, + (const QuerySpec& query, const Variant& variant), (override)); + MOCK_METHOD(void, UpdateServerCache, + (const Path& path, const CompoundWrite& children), (override)); + MOCK_METHOD(void, SetQueryActive, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetQueryInactive, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetQueryComplete, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetTrackedQueryKeys, + (const QuerySpec& query, const std::set& keys), + (override)); + MOCK_METHOD(void, UpdateTrackedQueryKeys, + (const QuerySpec& query, const std::set& added, + const std::set& removed), + (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_MANAGER_H_ diff --git a/database/tests/desktop/test/mock_persistence_storage_engine.h b/database/tests/desktop/test/mock_persistence_storage_engine.h new file mode 100644 index 0000000000..7db6674127 --- /dev/null +++ b/database/tests/desktop/test/mock_persistence_storage_engine.h @@ -0,0 +1,79 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_STORAGE_ENGINE_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_STORAGE_ENGINE_H_ + +#include + +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockPersistenceStorageEngine : public PersistenceStorageEngine { + public: + MOCK_METHOD(void, SaveUserOverwrite, + (const Path& path, const Variant& data, WriteId write_id), + (override)); + MOCK_METHOD(void, SaveUserMerge, + (const Path& path, const CompoundWrite& children, + WriteId write_id), + (override)); + MOCK_METHOD(void, RemoveUserWrite, (WriteId write_id), (override)); + MOCK_METHOD(std::vector, LoadUserWrites, (), (override)); + MOCK_METHOD(void, RemoveAllUserWrites, (), (override)); + MOCK_METHOD(Variant, ServerCache, (const Path& path), (override)); + MOCK_METHOD(void, OverwriteServerCache, + (const Path& path, const Variant& data), (override)); + MOCK_METHOD(void, MergeIntoServerCache, + (const Path& path, const Variant& data), (override)); + MOCK_METHOD(void, MergeIntoServerCache, + (const Path& path, const CompoundWrite& children), (override)); + MOCK_METHOD(uint64_t, ServerCacheEstimatedSizeInBytes, (), (const, override)); + MOCK_METHOD(void, SaveTrackedQuery, (const TrackedQuery& tracked_query), + (override)); + MOCK_METHOD(void, DeleteTrackedQuery, (QueryId tracked_query_id), (override)); + MOCK_METHOD(std::vector, LoadTrackedQueries, (), (override)); + MOCK_METHOD(void, ResetPreviouslyActiveTrackedQueries, (uint64_t last_use), + (override)); + MOCK_METHOD(void, SaveTrackedQueryKeys, + (QueryId tracked_query_id, const std::set& keys), + (override)); + MOCK_METHOD(void, UpdateTrackedQueryKeys, + (QueryId tracked_query_id, const std::set& added, + const std::set& removed), + (override)); + MOCK_METHOD(std::set, LoadTrackedQueryKeys, + (QueryId tracked_query_id), (override)); + MOCK_METHOD(std::set, LoadTrackedQueryKeys, + (const std::set& trackedQueryIds), (override)); + MOCK_METHOD(void, PruneCache, + (const Path& root, const PruneForestRef& prune_forest), + (override)); + MOCK_METHOD(bool, BeginTransaction, (), (override)); + MOCK_METHOD(void, EndTransaction, (), (override)); + MOCK_METHOD(void, SetTransactionSuccessful, (), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_STORAGE_ENGINE_H_ diff --git a/database/tests/desktop/test/mock_tracked_query_manager.h b/database/tests/desktop/test/mock_tracked_query_manager.h new file mode 100644 index 0000000000..0c5b0ccdab --- /dev/null +++ b/database/tests/desktop/test/mock_tracked_query_manager.h @@ -0,0 +1,52 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_TRACKED_QUERY_MANAGER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_TRACKED_QUERY_MANAGER_H_ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/tracked_query_manager.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockTrackedQueryManager : public TrackedQueryManagerInterface { + public: + MOCK_METHOD(const TrackedQuery*, FindTrackedQuery, (const QuerySpec& query), + (const, override)); + MOCK_METHOD(void, RemoveTrackedQuery, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetQueryActiveFlag, + (const QuerySpec& query, + TrackedQuery::ActivityStatus activity_status), + (override)); + MOCK_METHOD(void, SetQueryCompleteIfExists, (const QuerySpec& query), + (override)); + MOCK_METHOD(void, SetQueriesComplete, (const Path& path), (override)); + MOCK_METHOD(bool, IsQueryComplete, (const QuerySpec& query), (override)); + MOCK_METHOD(PruneForest, PruneOldQueries, (const CachePolicy& cache_policy), + (override)); + MOCK_METHOD(std::set, GetKnownCompleteChildren, + (const Path& path), (override)); + MOCK_METHOD(void, EnsureCompleteTrackedQuery, (const Path& path), (override)); + MOCK_METHOD(bool, HasActiveDefaultQuery, (const Path& path), (override)); + MOCK_METHOD(uint64_t, CountOfPrunableQueries, (), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_TRACKED_QUERY_MANAGER_H_ diff --git a/database/tests/desktop/test/mock_write_tree.h b/database/tests/desktop/test/mock_write_tree.h new file mode 100644 index 0000000000..5224ce0a7b --- /dev/null +++ b/database/tests/desktop/test/mock_write_tree.h @@ -0,0 +1,80 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_WRITE_TREE_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_WRITE_TREE_H_ + +#include +#include +#include "app/src/include/firebase/variant.h" +#include "app/src/optional.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/write_tree.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" +#include "database/src/desktop/view/view_cache.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockWriteTree : public WriteTree { + public: + MOCK_METHOD(Optional, CalcCompleteEventCache, + (const Path& tree_path, const Variant* complete_server_cache), + (const, override)); + + MOCK_METHOD(Optional, CalcCompleteEventCache, + (const Path& tree_path, const Variant* complete_server_cache, + const std::vector& write_ids_to_exclude), + (const, override)); + + MOCK_METHOD(Optional, CalcCompleteEventCache, + (const Path& tree_path, const Variant* complete_server_cache, + const std::vector& write_ids_to_exclude, + HiddenWriteInclusion include_hidden_writes), + (const, override)); + + MOCK_METHOD(Variant, CalcCompleteEventChildren, + (const Path& tree_path, const Variant& complete_server_children), + (const, override)); + + MOCK_METHOD(Optional, CalcEventCacheAfterServerOverwrite, + (const Path& tree_path, const Path& path, + const Variant* existing_local_snap, + const Variant* existing_server_snap), + (const, override)); + + MOCK_METHOD((Optional>), CalcNextVariantAfterPost, + (const Path& tree_path, + const Optional& complete_server_data, + (const std::pair& post), + IterationDirection direction, const QueryParams& params), + (const, override)); + + MOCK_METHOD(Optional, ShadowingWrite, (const Path& path), + (const, override)); + + MOCK_METHOD(Optional, CalcCompleteChild, + (const Path& tree_path, const std::string& child_key, + const CacheNode& existing_server_cache), + (const, override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_WRITE_TREE_H_ diff --git a/database/tests/desktop/util_desktop_test.cc b/database/tests/desktop/util_desktop_test.cc new file mode 100644 index 0000000000..70a86c65c8 --- /dev/null +++ b/database/tests/desktop/util_desktop_test.cc @@ -0,0 +1,2775 @@ +// Copyright 2017 Google LLC +// +// 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 "database/src/desktop/util_desktop.h" + +#include +#include + +#include +#include +#include +#if defined(_WIN32) +#include +static const char* kPathSep = "\\"; +#define unlink _unlink +#else +static const char* kPathSep = "//"; +#endif + +#include "app/src/include/firebase/variant.h" +#include "app/src/path.h" +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +using ::testing::Eq; +using ::testing::Pair; +using ::testing::Property; +using ::testing::StrEq; +using ::testing::UnorderedElementsAre; + +TEST(UtilDesktopTest, IsPriorityKey) { + EXPECT_FALSE(IsPriorityKey("")); + EXPECT_FALSE(IsPriorityKey("A")); + EXPECT_FALSE(IsPriorityKey(".priority_queue")); + EXPECT_FALSE(IsPriorityKey(".priority ")); + EXPECT_FALSE(IsPriorityKey(" .priority")); + EXPECT_TRUE(IsPriorityKey(".priority")); +} + +TEST(UtilDesktopTest, StringStartsWith) { + EXPECT_TRUE(StringStartsWith("abcde", "")); + EXPECT_TRUE(StringStartsWith("abcde", "abc")); + EXPECT_TRUE(StringStartsWith("abcde", "abcde")); + + EXPECT_FALSE(StringStartsWith("abcde", "zzzzz")); + EXPECT_FALSE(StringStartsWith("abcde", "aaaaa")); + EXPECT_FALSE(StringStartsWith("abcde", "cde")); + EXPECT_FALSE(StringStartsWith("abcde", "abcdefghijklmnopqrstuvwxyz")); +} + +TEST(UtilDesktopTest, MapGet) { + std::map string_map{ + std::make_pair("one", 1), + std::make_pair("two", 2), + std::make_pair("three", 3), + }; + + // Get a value that does exist, non-const. + EXPECT_EQ(*MapGet(&string_map, "one"), 1); + EXPECT_EQ(*MapGet(&string_map, std::string("one")), 1); + // Get a value that does not exist, non-const. + EXPECT_EQ(MapGet(&string_map, "zero"), nullptr); + EXPECT_EQ(MapGet(&string_map, std::string("zero")), nullptr); + // Get a value that does exist, const. + EXPECT_EQ(*MapGet(&string_map, "two"), 2); + EXPECT_EQ(*MapGet(&string_map, std::string("two")), 2); + // Get a value that does not exist, const. + EXPECT_EQ(MapGet(&string_map, "zero"), nullptr); + EXPECT_EQ(MapGet(&string_map, std::string("zero")), nullptr); +} + +TEST(UtilDesktopTest, Extend) { + { + std::vector a{1, 2, 3, 4}; + std::vector b{5, 6, 7, 8}; + + Extend(&a, b); + EXPECT_EQ(a, (std::vector{1, 2, 3, 4, 5, 6, 7, 8})); + } + { + std::vector a; + std::vector b{5, 6, 7, 8}; + + Extend(&a, b); + EXPECT_EQ(a, (std::vector{5, 6, 7, 8})); + } + { + std::vector a{1, 2, 3, 4}; + std::vector b; + + Extend(&a, b); + EXPECT_EQ(a, (std::vector{1, 2, 3, 4})); + } +} + +TEST(UtilDesktopTest, PatchVariant) { + std::map starting_map{ + std::make_pair("a", 1), + std::make_pair("b", 2), + std::make_pair("c", 3), + }; + + // Completely overlapping data. + { + std::map patch_map{ + std::make_pair("a", 10), + std::make_pair("b", 20), + std::make_pair("c", 30), + }; + Variant data(starting_map); + Variant patch_data(patch_map); + EXPECT_TRUE(PatchVariant(patch_data, &data)); + EXPECT_TRUE(data.is_map()); + EXPECT_THAT(data.map(), UnorderedElementsAre(Pair(Eq("a"), Eq(10)), + Pair(Eq("b"), Eq(20)), + Pair(Eq("c"), Eq(30)))); + } + + // Completely disjoint data. + { + std::map patch_map{ + std::make_pair("d", 40), + std::make_pair("e", 50), + std::make_pair("f", 60), + }; + Variant data(starting_map); + Variant patch_data(patch_map); + EXPECT_TRUE(PatchVariant(patch_data, &data)); + EXPECT_TRUE(data.is_map()); + EXPECT_THAT(data.map(), UnorderedElementsAre( + Pair(Eq("a"), Eq(1)), Pair(Eq("b"), Eq(2)), + Pair(Eq("c"), Eq(3)), Pair(Eq("d"), Eq(40)), + Pair(Eq("e"), Eq(50)), Pair(Eq("f"), Eq(60)))); + } + + // Partially overlapping data. + { + std::map patch_map{ + std::make_pair("a", 100), + std::make_pair("d", 400), + std::make_pair("f", 600), + }; + Variant data(starting_map); + Variant patch_data(patch_map); + EXPECT_TRUE(PatchVariant(patch_data, &data)); + EXPECT_TRUE(data.is_map()); + EXPECT_THAT(data.map(), UnorderedElementsAre( + Pair(Eq("a"), Eq(100)), Pair(Eq("b"), Eq(2)), + Pair(Eq("c"), Eq(3)), Pair(Eq("d"), Eq(400)), + Pair(Eq("f"), Eq(600)))); + } + + // Source data is not a map. + { + Variant data; + std::map patch_map{ + std::make_pair("a", 10), + std::make_pair("b", 20), + std::make_pair("c", 30), + }; + Variant patch_data(patch_map); + EXPECT_FALSE(PatchVariant(patch_data, &data)); + } + // Patch data is not a map. + { + Variant data(starting_map); + Variant patch_data; + + EXPECT_FALSE(PatchVariant(patch_data, &data)); + } + + // Neither source nor patch data is a map. + { + Variant data; + Variant patch_data; + EXPECT_FALSE(PatchVariant(patch_data, &data)); + } +} + +TEST(UtilDesktopTest, VariantGetChild) { + Variant null_variant; + EXPECT_EQ(VariantGetChild(&null_variant, Path()), Variant::Null()); + EXPECT_EQ(VariantGetChild(&null_variant, Path("aaa")), Variant::Null()); + EXPECT_EQ(VariantGetChild(&null_variant, Path("aaa/bbb")), Variant::Null()); + + Variant leaf_variant = 100; + EXPECT_EQ(VariantGetChild(&leaf_variant, Path()), 100); + EXPECT_EQ(VariantGetChild(&leaf_variant, Path("aaa")), Variant::Null()); + EXPECT_EQ(VariantGetChild(&leaf_variant, Path("aaa/bbb")), Variant::Null()); + + Variant prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, Path()), + Variant(std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + })); + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, Path("aaa")), + Variant::Null()); + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, Path("aaa/bbb")), + Variant::Null()); + + Variant map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + }), + }); + + EXPECT_EQ(VariantGetChild(&map_variant, Path()), map_variant); + EXPECT_EQ(VariantGetChild(&map_variant, Path("aaa")), 100); + EXPECT_EQ(VariantGetChild(&map_variant, Path("aaa/bbb")), Variant::Null()); + EXPECT_EQ(VariantGetChild(&map_variant, Path("bbb")), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + })); + EXPECT_EQ(VariantGetChild(&map_variant, Path("bbb/ccc")), Variant(200)); + + Variant prioritized_map_variant(std::map{ + std::make_pair(".priority", 1), + std::make_pair("aaa", + std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + }), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + std::make_pair(".priority", 3), + }), + }); + + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path()), + prioritized_map_variant); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("aaa")), + Variant(std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + })); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("aaa/bbb")), + Variant::Null()); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("bbb")), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + })); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("bbb/ccc")), + Variant(200)); +} + +TEST(UtilDesktopTest, VariantGetImmediateChild) { + Variant null_variant; + EXPECT_EQ(VariantGetChild(&null_variant, "aaa"), Variant::Null()); + EXPECT_EQ(VariantGetChild(&null_variant, ".priority"), Variant::Null()); + + Variant leaf_variant = 100; + EXPECT_EQ(VariantGetChild(&leaf_variant, "aaa"), Variant::Null()); + + Variant prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, "aaa"), Variant::Null()); + + Variant map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + }), + }); + + EXPECT_EQ(VariantGetChild(&map_variant, "aaa"), 100); + EXPECT_EQ(VariantGetChild(&map_variant, "bbb"), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + })); + + Variant prioritized_map_variant(std::map{ + std::make_pair(".priority", 1), + std::make_pair("aaa", + std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + }), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + std::make_pair(".priority", 3), + }), + }); + + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, "aaa"), + Variant(std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + })); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, "bbb"), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + })); +} + +TEST(UtilDesktopTest, VariantUpdateChild_NullVariant) { + Variant null_variant; + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(), 100); + EXPECT_EQ(null_variant, Variant(100)); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/bbb"), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::EmptyMap()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/bbb"), Variant::EmptyMap()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(".priority"), 100); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/.priority"), 100); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), 100); + EXPECT_EQ(null_variant, + Variant(std::map{std::make_pair("aaa", 100)})); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/bbb"), 1234); + EXPECT_EQ(null_variant, Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", 1234), + }), + })); +} + +TEST(UtilDesktopTest, VariantUpdateChild_LeafVariant) { + Variant leaf_variant = 100; + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path(), Variant::Null()); + EXPECT_EQ(leaf_variant, Variant::Null()); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path(), Variant(1234)); + EXPECT_EQ(leaf_variant, Variant(1234)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa/bbb"), Variant::Null()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa/bbb"), Variant::EmptyMap()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path(".priority"), 999); + EXPECT_EQ(leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa"), 1234); + EXPECT_EQ(leaf_variant, + Variant(std::map{std::make_pair("aaa", 1234)})); + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa/bbb"), 1234); + EXPECT_EQ(leaf_variant, Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", 1234), + }), + })); + + const Variant original_prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + Variant prioritized_leaf_variant; + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path(), Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, Variant::Null()); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path(), Variant(1234)); + EXPECT_EQ(prioritized_leaf_variant, Variant(1234)); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa/bbb"), + Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa/bbb"), + Variant::EmptyMap()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path(".priority"), 999); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa"), 1234); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 10), + std::make_pair("aaa", 1234), + })); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa/bbb"), 1234); + EXPECT_EQ(prioritized_leaf_variant, + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", 1234), + }), + std::make_pair(".priority", 10), + })); +} + +TEST(UtilDesktopTest, VariantUpdateChild_MapVariant) { + Variant original_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }); + Variant map_variant = original_map_variant; + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(), Variant::Null()); + EXPECT_EQ(map_variant, Variant::Null()); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(), Variant(9999)); + EXPECT_EQ(map_variant, Variant(9999)); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(".priority"), Variant::Null()); + EXPECT_EQ(map_variant, original_map_variant); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(".priority"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("aaa"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("bbb"), Variant::Null()); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("bbb"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("ccc"), Variant::Null()); + EXPECT_EQ(map_variant, original_map_variant); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("ccc"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + })); + + Variant original_prioritized_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + }); + Variant prioritized_map_variant; + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, Variant::Null()); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, Variant(9999)); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(".priority"), + Variant::Null()); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(".priority"), + Variant(9999)); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("aaa"), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("bbb"), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("bbb"), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("ccc"), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, original_prioritized_map_variant); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("ccc"), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + std::make_pair(".priority", 1234), + })); +} + +TEST(UtilDesktopTest, VariantUpdateImmediateChild_NullVariant) { + Variant null_variant; + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::EmptyMap()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(".priority"), 100); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), 100); + EXPECT_EQ(null_variant, + Variant(std::map{std::make_pair("aaa", 100)})); +} + +TEST(UtilDesktopTest, VariantUpdateImmediateChild_LeafVariant) { + const Variant original_leaf_variant = 100; + Variant leaf_variant; + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, "aaa", Variant::Null()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, "aaa", Variant::EmptyMap()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, ".priority", 999); + EXPECT_EQ(leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, "aaa", 1234); + EXPECT_EQ(leaf_variant, + Variant(std::map{std::make_pair("aaa", 1234)})); + + const Variant original_prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + Variant prioritized_leaf_variant; + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, "aaa", Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, "aaa", Variant::EmptyMap()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, ".priority", 999); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, "aaa", 1234); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 10), + std::make_pair("aaa", 1234), + })); +} + +TEST(UtilDesktopTest, VariantUpdateImmediateChild_MapVariant) { + Variant original_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }); + Variant map_variant; + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, ".priority", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, "aaa", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, "bbb", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, "ccc", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + })); + + Variant original_prioritized_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + }); + Variant prioritized_map_variant; + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, ".priority", 9999); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, "aaa", 9999); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, "bbb", 9999); + EXPECT_EQ(prioritized_map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, "ccc", 9999); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + std::make_pair(".priority", 1234), + })); +} + +TEST(UtilDesktopTest, GetVariantAtPath) { + std::map candy{}; + std::map fruits{ + std::make_pair("apple", "red"), + std::make_pair("banana", "yellow"), + std::make_pair("grape", "purple"), + }; + std::map vegetables{ + std::make_pair(".value", std::map{ + std::make_pair("broccoli", "green"), + std::make_pair("carrot", "orange"), + std::make_pair("cauliflower", "white"), + })}; + std::map healthy_food_map{ + std::make_pair("candy", candy), + std::make_pair("fruits", fruits), + std::make_pair("vegetables", vegetables), + }; + + // Get root value. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, Path::GetRoot()); + EXPECT_EQ(result, &healthy_food); + } + + // Get valid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, Path("fruits")); + EXPECT_EQ(result, &healthy_food.map()["fruits"]); + } + + // Get valid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + GetInternalVariant(&healthy_food, Path("vegetables/carrot")); + EXPECT_EQ( + result, + &healthy_food.map()["vegetables"].map()[".value"].map()["carrot"]); + } + + // Get invalid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, Path("cereal")); + EXPECT_EQ(result, nullptr); + } + + // Get invalid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + GetInternalVariant(&healthy_food, Path("candy/marshmallows")); + EXPECT_EQ(result, nullptr); + } + + // Attempt to retrieve something from a non-map. + { + Variant not_a_map(100); + Variant* result = GetInternalVariant(¬_a_map, Path("fruits")); + EXPECT_EQ(result, nullptr); + } +} + +TEST(UtilDesktopTest, GetVariantAtKey) { + std::map candy{}; + std::map fruits{ + std::make_pair("apple", "red"), + std::make_pair("banana", "yellow"), + std::make_pair("grape", "purple"), + }; + std::map vegetables{ + std::make_pair("broccoli", "green"), + std::make_pair("carrot", "orange"), + std::make_pair("cauliflower", "white"), + }; + std::map healthy_food_map{ + std::make_pair(".value", + std::map{ + std::make_pair("candy", candy), + std::make_pair("fruits", fruits), + std::make_pair("vegetables", vegetables), + }), + }; + + // Get valid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, "fruits"); + EXPECT_EQ(result, &healthy_food.map()[".value"].map()["fruits"]); + } + + // Try and fail to get a grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, "vegetables/carrot"); + EXPECT_EQ(result, nullptr); + } + + // Get invalid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, "cereal"); + EXPECT_EQ(result, nullptr); + } + + // Attempt to retrieve something from a non-map. + { + Variant not_a_map(100); + Variant* result = GetInternalVariant(¬_a_map, "fruits"); + EXPECT_EQ(result, nullptr); + } +} + +TEST(UtilDesktopTest, MakeVariantAtPath) { + std::map healthy_food_map{ + std::make_pair("candy", std::map{}), + std::make_pair("fruits", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("banana", "yellow"), + std::make_pair("grape", "purple"), + }), + std::make_pair("vegetables", + std::map{ + std::make_pair("broccoli", "green"), + std::make_pair("carrot", "orange"), + std::make_pair("cauliflower", + std::map{ + std::make_pair(".value", "white"), + std::make_pair(".priority", 100), + }), + }), + }; + + // Get root value. + { + Variant healthy_food(healthy_food_map); + Variant* result = MakeVariantAtPath(&healthy_food, Path::GetRoot()); + EXPECT_EQ(*result, healthy_food); + } + + // Get valid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = MakeVariantAtPath(&healthy_food, Path("fruits")); + EXPECT_EQ(result, &healthy_food.map()["fruits"]); + } + + // Get valid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("vegetables/carrot")); + EXPECT_EQ(result, &healthy_food.map()["vegetables"].map()["carrot"]); + } + + // Get invalid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = MakeVariantAtPath(&healthy_food, Path("cereal")); + EXPECT_EQ(result, &healthy_food.map()["cereal"]); + EXPECT_TRUE(healthy_food.map()["candy"].is_map()); + EXPECT_EQ(healthy_food.map()["candy"].map().size(), 0); + } + + // Get invalid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("candy/marshmallows")); + EXPECT_NE(result, nullptr); + EXPECT_TRUE(healthy_food.is_map()); + EXPECT_TRUE(healthy_food.map()["candy"].is_map()); + EXPECT_NE(healthy_food.map()["candy"].map().find("marshmallows"), + healthy_food.map()["candy"].map().end()); + EXPECT_EQ(result, &healthy_food.map()["candy"].map()["marshmallows"]); + } + + // Attempt to retrieve something from a non-map. + { + Variant not_a_map(100); + Variant* result = MakeVariantAtPath(¬_a_map, Path("fruits")); + EXPECT_TRUE(not_a_map.is_map()); + EXPECT_EQ(result, ¬_a_map.map()["fruits"]); + EXPECT_NE(not_a_map.map().find("fruits"), not_a_map.map().end()); + } + + // Attempt to retrieve a node with a ".value". + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("vegetables/cauliflower")); + EXPECT_NE(result, nullptr); + EXPECT_EQ(*result, Variant(std::map{ + std::make_pair(".value", "white"), + std::make_pair(".priority", 100), + })); + } + + // Attempt to retrieve a node past a ".value". + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("vegetables/cauliflower/new")); + EXPECT_NE(result, nullptr); + EXPECT_EQ( + result, + &healthy_food.map()["vegetables"].map()["cauliflower"].map()["new"]); + EXPECT_EQ(healthy_food.map()["vegetables"] + .map()["cauliflower"] + .map()[".priority"], + 100); + EXPECT_EQ(*result, Variant::Null()); + } +} + +TEST(UtilDesktopTest, SetVariantAtPath) { + Variant initial = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 300), + std::make_pair(".priority", 999), + }), + }), + }; + + // Change existing value + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/bbb"), 1000); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 1000), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 300), + std::make_pair(".priority", 999), + }), + }), + }; + EXPECT_EQ(variant, expected); + } + + // Change existing value inside of a .value key. + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/ddd"), 3000); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 3000), + std::make_pair(".priority", 999), + }), + }), + }; + EXPECT_EQ(variant, expected); + } + + // Add a new value. + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/eee"), 4000); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 300), + std::make_pair(".priority", 999), + }), + std::make_pair("eee", 4000), + }), + }; + EXPECT_EQ(variant, expected); + } + + // Add map at a location with a .value + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/ddd"), + std::map{ + std::make_pair("zzz", 999), + std::make_pair("yyy", 888), + }); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair("zzz", 999), + std::make_pair("yyy", 888), + std::make_pair(".priority", 999), + }), + }), + }; + EXPECT_EQ(variant, expected); + } +} + +TEST(UtilDesktopTest, ParseUrlSupportCases) { + // Without Path + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com/"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test-123.firebaseio.com"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test-123.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test-123"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("http://test.firebaseio.com"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_FALSE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com"), ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com/"), ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com:80"), ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com:80"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com:8080/"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com:8080"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + // With path + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com/path/to/key"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, "path/to/key"); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com/path/to/key/"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, "path/to/key/"); + } +} + +TEST(UtilDesktopTest, ParseUrlErrorCases) { + // Test Wrong Protocol + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("://"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("://test.firebaseio.com"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("ws://test.firebaseio.com"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("ftp://test.firebaseio.com"), + ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("https:/test.firebaseio.com"), + ParseUrl::kParseOk); + } + + // Test wrong port + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:44a"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:a"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:a43"), ParseUrl::kParseOk); + } + + // Test Wrong hostname/namespace + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse(""), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http:///"), ParseUrl::kParseOk); // NOTYPO + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://./"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://a."), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://a....../"), ParseUrl::kParseOk); + } +} + +TEST(UtilDesktopTest, CountChildren_Fundamental_Type) { + Variant simple_value = 10; + EXPECT_EQ(CountEffectiveChildren(simple_value), 0); + + std::map children; + std::map expect_children; + EXPECT_THAT(GetEffectiveChildren(simple_value, &children), Eq(0)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, CountChildren_FundamentalTypeWithPriority) { + Variant high_priority_food = std::map{ + std::make_pair(".value", "milk chocolate"), + std::make_pair(".priority", 10000), + }; + EXPECT_EQ(CountEffectiveChildren(high_priority_food), 0); + + std::map children; + std::map expect_children; + EXPECT_THAT(GetEffectiveChildren(high_priority_food, &children), Eq(0)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, CountChildren_MapWithPriority) { + // Remove priority field. + Variant worst_foods_with_priority = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + std::make_pair(".priority", -100000), + }; + EXPECT_EQ(CountEffectiveChildren(worst_foods_with_priority), 3); + + std::map children; + std::map expect_children = { + std::make_pair("bad", &worst_foods_with_priority.map()["bad"]), + std::make_pair("badder", &worst_foods_with_priority.map()["badder"]), + std::make_pair("baddest", &worst_foods_with_priority.map()["baddest"]), + }; + EXPECT_THAT(GetEffectiveChildren(worst_foods_with_priority, &children), + Eq(3)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, CountChildren_MapWithoutPriority) { + // Remove priority field. + Variant worst_foods = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + }; + EXPECT_EQ(CountEffectiveChildren(worst_foods), 3); + + std::map children; + std::map expect_children = { + std::make_pair("bad", &worst_foods.map()["bad"]), + std::make_pair("badder", &worst_foods.map()["badder"]), + std::make_pair("baddest", &worst_foods.map()["baddest"]), + }; + EXPECT_THAT(GetEffectiveChildren(worst_foods, &children), Eq(3)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, HasVector) { + EXPECT_FALSE(HasVector(Variant(10))); + EXPECT_FALSE(HasVector(Variant("A"))); + EXPECT_FALSE(HasVector(util::JsonToVariant("{\"A\":1}"))); + EXPECT_TRUE(HasVector(util::JsonToVariant("[1,2,3]"))); + EXPECT_TRUE(HasVector(util::JsonToVariant("{\"A\":[1,2,3]}"))); +} + +TEST(UtilDesktopTest, ParseInteger) { + int64_t number = 0; + EXPECT_TRUE(ParseInteger("0", &number)); + EXPECT_EQ(number, 0); + EXPECT_TRUE(ParseInteger("1", &number)); + EXPECT_EQ(number, 1); + EXPECT_TRUE(ParseInteger("-1", &number)); + EXPECT_EQ(number, -1); + EXPECT_TRUE(ParseInteger("+1", &number)); + EXPECT_EQ(number, 1); + EXPECT_TRUE(ParseInteger("1234", &number)); + EXPECT_EQ(number, 1234); + + EXPECT_TRUE(ParseInteger("00", &number)); + EXPECT_EQ(number, 0); + EXPECT_TRUE(ParseInteger("01", &number)); + EXPECT_EQ(number, 1); + EXPECT_TRUE(ParseInteger("-01", &number)); + EXPECT_EQ(number, -1); + + EXPECT_FALSE(ParseInteger("1234.1", &number)); + EXPECT_FALSE(ParseInteger("1 2 3", &number)); + EXPECT_FALSE(ParseInteger("ABC", &number)); + EXPECT_FALSE(ParseInteger("1B3", &number)); + EXPECT_FALSE(ParseInteger("123.A", &number)); +} + +TEST(UtilDesktopTest, PrunePrioritiesAndConvertVector) { + { + // 10 => 10 + Variant value = 10; + Variant expect = value; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {".value":10, ".priority":1} => 10 + Variant value = util::JsonToVariant("{\".value\":10,\".priority\":1}"); + Variant expect = 10; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":10, ".priority":1} => {"A":10} + Variant value = util::JsonToVariant("{\"A\":10,\".priority\":1}"); + Variant expect = util::JsonToVariant("{\"A\":10}"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":{"B":10,".priority":2},".priority":1} => {"A":{"B":10}} + Variant value = util::JsonToVariant( + "{\"A\":{\"B\":10,\".priority\":2},\".priority\":1}"); + Variant expect = util::JsonToVariant("{\"A\":{\"B\":10}}"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"1":1,"2":2} => [0,1,2] + Variant value = util::JsonToVariant("{\"0\":0,\"1\":1,\"2\":2}"); + Variant expect = util::JsonToVariant("[0,1,2]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"000000":0,"000001":1,"000002":2} => {"000000":0,"000001":1,"000002":2} + Variant value = + util::JsonToVariant("{\"000000\":0,\"000001\":1,\"000002\":2}"); + Variant expect = + util::JsonToVariant("{\"000000\":0,\"000001\":1,\"000002\":2}"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"2":2} => [0,null,2] + Variant value = util::JsonToVariant("{\"0\":0,\"2\":2}"); + Variant expect = util::JsonToVariant("[0,null,2]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // No change because more than half of the keys are missing (1, 2, 3) + // {"3":3} => {"3":3} + Variant value = util::JsonToVariant("{\"3\":3}"); + Variant expect = value; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // Change because less or equal to half of the keys are missing (0, 2) + // {"1":1,"3":3} => [null,1,null,3] + Variant value = util::JsonToVariant("{\"1\":1,\"3\":3}"); + Variant expect = util::JsonToVariant("[null,1,null,3]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"1":1,"A":"2"} => {"0":0,"1":1,"A":"2"} + Variant value = util::JsonToVariant("{\"0\":0,\"1\":1,\"A\":\"2\"}"); + Variant expect = value; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"1":1,".priority":1} => [0,1] + Variant value = util::JsonToVariant("{\"0\":0,\"1\":1,\".priority\":1}"); + Variant expect = util::JsonToVariant("[0,1]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":{"0":0,".priority":1},"1":1,".priority":1} => [[0],1] + Variant value = util::JsonToVariant( + "{\"0\":{\"0\":0,\".priority\":1},\"1\":1,\".priority\":1}"); + Variant expect = util::JsonToVariant("[[0],1]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } +} + +TEST(UtilDesktopTest, PruneNullsRecursively) { + Variant value = std::map{ + std::make_pair("null", Variant::Null()), + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair( + "map", + std::map{ + std::make_pair("another_null", Variant::Null()), + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + std::make_pair("another_empty_map", Variant::EmptyMap()), + }), + std::make_pair("empty_map", Variant::EmptyMap()), + }; + + PruneNulls(&value, true); + + Variant expected = std::map{ + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair("map", + std::map{ + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + }), + }; + + EXPECT_EQ(value, expected); +} + +TEST(UtilDesktopTest, PruneNullsNonRecursively) { + Variant value = std::map{ + std::make_pair("null", Variant::Null()), + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair( + "map", + std::map{ + std::make_pair("another_null", Variant::Null()), + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + std::make_pair("another_empty_map", Variant::EmptyMap()), + }), + std::make_pair("empty_map", Variant::EmptyMap()), + }; + + PruneNulls(&value, false); + + Variant expected = std::map{ + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair( + "map", + std::map{ + std::make_pair("another_null", Variant::Null()), + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + std::make_pair("another_empty_map", Variant::EmptyMap()), + }), + }; + + EXPECT_EQ(value, expected); +} + +TEST(UtilDesktopTest, ConvertVectorToMap) { + { + // 10 => 10 + Variant value = 10; + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {".value":10, ".priority":1} => {".value":10, ".priority":1} + Variant value = util::JsonToVariant("{\".value\":10,\".priority\":1}"); + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":10, ".priority":1} => {"A":10, ".priority":1} + Variant value = util::JsonToVariant("{\"A\":10,\".priority\":1}"); + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":{"B":10,".priority":2},".priority":1} => + // {"A":{"B":10,".priority":2},".priority":1} + Variant value = util::JsonToVariant( + "{\"A\":{\"B\":10,\".priority\":2},\".priority\":1}"); + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // [0,1,2] => {"0":0,"1":1,"2":2} + Variant value = util::JsonToVariant("[0,1,2]"); + Variant expect = util::JsonToVariant("{\"0\":0,\"1\":1,\"2\":2}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // [[0,1],1,2] => {"0":{"0":0,"1":1},"1":1,"2":2} + Variant value = util::JsonToVariant("[[0,1],1,2]"); + Variant expect = + util::JsonToVariant("{\"0\":{\"0\":0,\"1\":1},\"1\":1,\"2\":2}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":[0,1],".priority":1} => {"0":{"0":0,"1":1},".priority":1} + Variant value = util::JsonToVariant("{\"0\":[0,1],\".priority\":1}"); + Variant expect = + util::JsonToVariant("{\"0\":{\"0\":0,\"1\":1},\".priority\":1}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {".value":[0,1,2],".priority":1} => {"0":0,"1":1,"2":2,".priority":1} + Variant value = util::JsonToVariant("{\".value\":[0,1,2],\".priority\":1}"); + Variant expect = + util::JsonToVariant("{\"0\":0,\"1\":1,\"2\":2,\".priority\":1}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // Test for sanity + // {".value":[{".value":[0,1],".priority":3},1,2],".priority":1} => + // {"0":{"0":0,"1":1,".priority":3},"1":1,"2":2,".priority":1} + Variant value = util::JsonToVariant( + "{\".value\":[{\".value\":[0,1],\".priority\":3},1,2],\".priority\":" + "1}"); + Variant expect = util::JsonToVariant( + "{\"0\":{\"0\":0,\"1\":1,\".priority\":3},\"1\":1,\"2\":2,\"." + "priority\":1}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } +} + +TEST(UtilDesktopTest, PrunePriorities_FundamentalType) { + // Ensure nothing happens. + Variant simple_value = 10; + Variant simple_value_copy = simple_value; + PrunePriorities(&simple_value); + EXPECT_EQ(simple_value, simple_value_copy); +} + +TEST(UtilDesktopTest, PrunePriorities_FundamentalTypeWithPriority) { + // Collapse the value/priority pair into just a value. + Variant high_priority_food = std::map{ + std::make_pair(".value", "pizza"), + std::make_pair(".priority", 10000), + }; + PrunePriorities(&high_priority_food); + EXPECT_THAT(high_priority_food.string_value(), StrEq("pizza")); +} + +TEST(UtilDesktopTest, PrunePriorities_MapWithPriority) { + // Remove priority field. + Variant worst_foods_with_priority = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + std::make_pair(".priority", -100000), + }; + Variant worst_foods = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + }; + PrunePriorities(&worst_foods_with_priority); + EXPECT_EQ(worst_foods_with_priority, worst_foods); +} + +TEST(UtilDesktopTest, PrunePriorities_NestedMaps) { + // Correctly handle recursive maps. + Variant nested_map = std::map{ + std::make_pair("simple_value", 1), + std::make_pair("prioritized_value", + std::map{ + std::make_pair(".value", "pizza"), + std::make_pair(".priority", 10000), + }), + std::make_pair("prioritized_map", + std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + std::make_pair(".priority", -100000), + }), + }; + Variant nested_map_expectation = std::map{ + std::make_pair("simple_value", 1), + std::make_pair("prioritized_value", "pizza"), + std::make_pair("prioritized_map", + std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + }), + }; + PrunePriorities(&nested_map); + EXPECT_EQ(nested_map, nested_map_expectation); +} + +TEST(UtilDesktopTest, GetVariantValueAndGetVariantPriority) { + // Test with Null priority + { + // Pairs of value and expected result + std::vector> test_cases = { + {"", ""}, // Variant::Null() + {"123", "123"}, + {"123.456", "123.456"}, + {"'string'", "'string'"}, + {"true", "true"}, + {"false", "false"}, + {"[1,2,3]", "[1,2,3]"}, + {"{'A':1,'B':'b','C':true}", "{'A':1,'B':'b','C':true}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant original_variant = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + + const Variant* value_ptr = GetVariantValue(&original_variant); + const Variant priority = GetVariantPriority(original_variant); + + EXPECT_NE(value_ptr, nullptr); + EXPECT_EQ(value_ptr, &original_variant); + EXPECT_EQ(*value_ptr, expected); + + EXPECT_EQ(priority, Variant::Null()); + } + } + + // Test with priority + { + // Pairs of value and expected result + std::vector> test_cases = { + {"{'.value':123,'.priority':100}", "123"}, + {"{'.value':123.456,'.priority':100}", "123.456"}, + {"{'.value':'string','.priority':100}", "'string'"}, + {"{'.value':true,'.priority':100}", "true"}, + {"{'.value':false,'.priority':100}", "false"}, + {"{'.value':[1,2,3],'.priority':100}", "[1,2,3]"}, + {"{'A':1,'B':'b','C':true,'.priority':100}", + "{'A':1,'B':'b','C':true,'.priority':100}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant original_variant = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + + const Variant* value_ptr = GetVariantValue(&original_variant); + const Variant& priority = GetVariantPriority(original_variant); + + EXPECT_TRUE(value_ptr != nullptr); + switch (value_ptr->type()) { + case Variant::kTypeNull: + case Variant::kTypeMap: + EXPECT_EQ(value_ptr, &original_variant); + break; + default: + EXPECT_EQ(value_ptr, &original_variant.map()[".value"]); + break; + } + EXPECT_EQ(*value_ptr, expected); + + EXPECT_EQ(priority, original_variant.map()[".priority"]); + EXPECT_EQ(priority, Variant::FromInt64(100)); + } + } +} + +TEST(UtilDesktopTest, CombineValueAndPriority) { + // Test with Null priority + { + Variant priority = Variant::Null(); + // Pairs of value and expected result. + std::vector> test_cases = { + {"", ""}, // Variant::Null() + {"123", "123"}, + {"123.456", "123.456"}, + {"'string'", "'string'"}, + {"true", "true"}, + {"false", "false"}, + {"[1,2,3]", "[1,2,3]"}, + {"{'A':1,'B':'b','C':true}", "{'A':1,'B':'b','C':true}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant value = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + EXPECT_THAT(CombineValueAndPriority(value, priority), Eq(expected)); + } + } + + // Test with priority + { + Variant priority = Variant::FromInt64(100); + // Pairs of value and expected result + std::vector> test_cases = { + {"", ""}, // Variant::Null() + {"123", "{'.value':123,'.priority':100}"}, + {"123.456", "{'.value':123.456,'.priority':100}"}, + {"'string'", "{'.value':'string','.priority':100}"}, + {"true", "{'.value':true,'.priority':100}"}, + {"false", "{'.value':false,'.priority':100}"}, + {"[1,2,3]", "{'.value':[1,2,3],'.priority':100}"}, + {"{'A':1,'B':'b','C':true}", + "{'A':1,'B':'b','C':true,'.priority':100}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant value = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + EXPECT_THAT(CombineValueAndPriority(value, priority), Eq(expected)); + } + } +} + +TEST(UtilDesktopTest, VariantIsLeaf) { + // Pairs of value and expected result + std::vector> test_cases = { + {"", true}, + {"123", true}, + {"123.456", true}, + {"'string'", true}, + {"true", true}, + {"false", true}, + {"[1,2,3]", false}, + {"{'A':1,'B':'b','C':true}", false}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", false}, + {"{'.value':123,'.priority':100}", true}, + {"{'.value':123.456,'.priority':100}", true}, + {"{'.value':'string','.priority':100}", true}, + {"{'.value':true,'.priority':100}", true}, + {"{'.value':false,'.priority':100}", true}, + {"{'.value':[1,2,3],'.priority':100}", false}, + {"{'A':1,'B':'b','C':true,'.priority':100}", false}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}", + false}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + + Variant original_variant = util::JsonToVariant(test.first.c_str()); + + EXPECT_THAT(VariantIsLeaf(original_variant), test.second); + } +} + +TEST(UtilDesktopTest, VariantIsEmpty) { + EXPECT_TRUE(VariantIsEmpty(Variant::Null())); + EXPECT_TRUE(VariantIsEmpty(Variant::EmptyMap())); + EXPECT_TRUE(VariantIsEmpty(Variant::EmptyVector())); + + EXPECT_FALSE(VariantIsEmpty(Variant::FromBool(false))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromBool(true))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromInt64(0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromInt64(9999))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromDouble(0.0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromDouble(1234.0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableString(""))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableString("lorem ipsum"))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromStaticString(""))); + EXPECT_FALSE(VariantIsEmpty( + Variant(std::map{std::make_pair("test", 10)}))); + EXPECT_FALSE(VariantIsEmpty(Variant(std::vector{1, 2, 3}))); + const char blob[] = {72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}; + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableBlob(nullptr, 0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableBlob(blob, sizeof(blob)))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromStaticBlob(nullptr, 0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromStaticBlob(blob, sizeof(blob)))); +} + +TEST(UtilDesktopTest, VariantsAreEquivalent) { + // All of the regular comparisons should behave as expected. + EXPECT_TRUE(VariantsAreEquivalent(Variant::Null(), Variant::Null())); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromBool(false), + Variant::FromBool(false))); + EXPECT_TRUE( + VariantsAreEquivalent(Variant::FromBool(true), Variant::FromBool(true))); + EXPECT_TRUE( + VariantsAreEquivalent(Variant::FromInt64(100), Variant::FromInt64(100))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromInt64(100), + Variant::FromDouble(100.0f))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromMutableString("Hi"), + Variant::FromMutableString("Hi"))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromStaticString("Hi"), + Variant::FromStaticString("Hi"))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromStaticString("Hi"), + Variant::FromMutableString("Hi"))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromMutableString("Hi"), + Variant::FromStaticString("Hi"))); + + // Double to Int comparison should result in equal values despite different + // types. + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromDouble(100.0f), + Variant::FromInt64(100))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromInt64(100), + Variant::FromDouble(100.0f))); + + EXPECT_FALSE(VariantsAreEquivalent(Variant::FromDouble(1000.0f), + Variant::FromInt64(100))); + EXPECT_FALSE( + VariantsAreEquivalent(Variant::FromDouble(3.14f), Variant::FromInt64(3))); + + // Maps should recursively check if children are also equivlanet. + Variant map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant equal_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant equivalent_variant = std::map{ + std::make_pair("aaa", 100.0), + std::make_pair("bbb", 200.0), + std::make_pair("ccc", 300.0), + }; + Variant priority_variant = std::map{ + std::make_pair(".priority", 1), + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + EXPECT_TRUE(VariantsAreEquivalent(map_variant, equal_variant)); + EXPECT_TRUE(VariantsAreEquivalent(map_variant, equivalent_variant)); + EXPECT_FALSE(VariantsAreEquivalent(map_variant, priority_variant)); + + // Strings are not the same as ints to the database + Variant bad_string_variant = std::map{ + std::make_pair("aaa", "100"), + std::make_pair("bbb", "200"), + std::make_pair("ccc", "300"), + }; + // Variants that have too many elements should not compare equal, even if + // the elements they share are the same. + Variant too_long_variant = std::map{ + std::make_pair("aaa", "100"), + std::make_pair("bbb", "200"), + std::make_pair("ccc", "300"), + std::make_pair("ddd", "400"), + }; + EXPECT_FALSE(VariantsAreEquivalent(map_variant, bad_string_variant)); + EXPECT_FALSE(VariantsAreEquivalent(map_variant, too_long_variant)); + + // Same rules should apply to nested variants. + Variant nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }; + Variant equal_nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }; + Variant equivalent_nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300.0), + std::make_pair("eee", 400.0), + }), + }; + + EXPECT_TRUE(VariantsAreEquivalent(nested_variant, equal_nested_variant)); + EXPECT_TRUE(VariantsAreEquivalent(nested_variant, equivalent_nested_variant)); + + Variant bad_nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300.0), + std::make_pair("eee", 400.0), + std::make_pair("fff", 500.0), + }), + }; + EXPECT_FALSE(VariantsAreEquivalent(nested_variant, bad_nested_variant)); +} + +TEST(UtilDesktopTest, GetBase64SHA1) { + std::vector> test_cases = { + {"", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="}, + {"i", "BC3EUS+j05HFFwzzqmHmpjj4Q0I="}, + {"ii", "ORg3PPVVnFS1LHBmQo9sQRjTHCM="}, + {"iii", "Ql/8FCLcTzJSi9n9WvNV/bXJYZI="}, + {"iiii", "MFMcKIXOYbOF3IHSo3X2vvgGB9U="}, + {"αβγωΑΒΓΩ", "WtUIYTivR0gge33nOEyQiBZGkmM="}, + }; + + std::string encoded; + for (auto& test : test_cases) { + EXPECT_THAT(GetBase64SHA1(test.first, &encoded), Eq(test.second)); + } +} + +TEST(UtilDesktopTest, ChildKeyCompareTo) { + // Expect left is equal to right + EXPECT_EQ(ChildKeyCompareTo(Variant("0"), Variant("0")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("1"), Variant("1")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("10"), Variant("10")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("A"), Variant("A")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("1A"), Variant("1A")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("[MIN_KEY]")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("[MAX_KEY]")), 0); + + // Expect left is greater than right + EXPECT_GT(ChildKeyCompareTo(Variant("1"), Variant("0")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("0"), Variant("-1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1"), Variant("-1")), 0); + // "001" is equivalant to "1" in int value + EXPECT_GT(ChildKeyCompareTo(Variant("001"), Variant("-1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1"), Variant("-001")), 0); + // "001" is equivalant to "1" in int value but has longer length as a string + EXPECT_GT(ChildKeyCompareTo(Variant("001"), Variant("1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-001"), Variant("-1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("001"), Variant("-001")), 0); + // String is always greater than int + EXPECT_GT(ChildKeyCompareTo(Variant("A"), Variant("1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1A"), Variant("10")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-1A"), Variant("10")), 0); + // "-" is a string + EXPECT_GT(ChildKeyCompareTo(Variant("-"), Variant("10")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-"), Variant("-1")), 0); + // "1.1" is not an int, therefore treated as a string + EXPECT_GT(ChildKeyCompareTo(Variant("1.1"), Variant("10")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1.1"), Variant("0")), 0); + // Floating point is treated as string for comparison. + EXPECT_GT(ChildKeyCompareTo(Variant("11.1"), Variant("1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1.1"), Variant("-1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-11.1"), Variant("-1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A"), Variant("1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A1"), Variant("A")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A2"), Variant("A1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("AA"), Variant("A")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("AA"), Variant("A1")), 0); + // "[MIN_KEY]" is less than anything + EXPECT_GT(ChildKeyCompareTo(Variant("0"), Variant("[MIN_KEY]")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-100000"), Variant("[MIN_KEY]")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("100000"), Variant("[MIN_KEY]")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A"), Variant("[MIN_KEY]")), 0); + // "[MAX_KEY]" is greater than anything + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("0")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("1000000")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("-1000000")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("A")), 0); + + // Expect left is less than right + EXPECT_LT(ChildKeyCompareTo(Variant("0"), Variant("1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("0")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("1")), 0); + // "001" is equivalant to "1" in int value + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("001")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-001"), Variant("1")), 0); + // "001" is equivalant to "1" in int value but has longer length as a string + EXPECT_LT(ChildKeyCompareTo(Variant("1"), Variant("001")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("-001")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-001"), Variant("001")), 0); + // String is always greater than int + EXPECT_LT(ChildKeyCompareTo(Variant("1"), Variant("A")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("1A")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("-1A")), 0); + // "-" is a string + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("-")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("-")), 0); + // "1.1" is not an int, therefore treated as a string + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("1.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("0"), Variant("1.1")), 0); + // Floating point is treated as string for comparison. + EXPECT_LT(ChildKeyCompareTo(Variant("1.1"), Variant("11.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1.1"), Variant("1.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1.1"), Variant("-11.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("1.1"), Variant("A")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A"), Variant("A1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A1"), Variant("A2")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A"), Variant("AA")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A1"), Variant("AA")), 0); + // "[MIN_KEY]" is less than anything + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("0")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("-100000")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("100000")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("A")), 0); + // "[MAX_KEY]" is greater than anything + EXPECT_LT(ChildKeyCompareTo(Variant("0"), Variant("[MAX_KEY]")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("100000"), Variant("[MAX_KEY]")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-100000"), Variant("[MAX_KEY]")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A"), Variant("[MAX_KEY]")), 0); +} + +TEST(UtilDesktopTest, GetHashRepresentation) { + std::vector> test_cases = { + // Null + {Variant::Null(), ""}, + // Int64 + {Variant(0), "number:0000000000000000"}, + {Variant(1), "number:3ff0000000000000"}, + {Variant::FromInt64(INT64_MIN), "number:c3e0000000000000"}, + // Double + {Variant(0.1), "number:3fb999999999999a"}, + {Variant(1.2345678901234567), "number:3ff3c0ca428c59fb"}, + {Variant(12345.678901234567), "number:40c81cd6e63c53d7"}, + {Variant(1234567890123456.5), "number:43118b54f22aeb02"}, + // Boolean + {Variant(true), "boolean:true"}, + {Variant(false), "boolean:false"}, + // String + {Variant("i"), "string:i"}, + {Variant("ii"), "string:ii"}, + {Variant("iii"), "string:iii"}, + {Variant("iiii"), "string:iiii"}, + // UTF-8 String + {Variant("αβγωΑΒΓΩ"), "string:αβγωΑΒΓΩ"}, + // Basic Map + {util::JsonToVariant("{\"B2\":2,\"B1\":1}"), + ":B1:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:B2:WtSt2Xo3L0JtPuArzQHofPrZOuU="}, + // Map with priority + {util::JsonToVariant( + "{\"B1\":{\".value\":1,\".priority\":2.0},\"B2\":{\".value\":2," + "\".priority\":1.0},\"B3\":3}"), + ":B3:3tYODYzGXwaGnXNech4jb4T9las=:B2:iiz9CIvYWkKdETTpjVFBJNx1SiI=" + ":B1:FvGzv2x5RbRTIc6uhMwY3pMW2oU="}, + // Array + {util::JsonToVariant("[1, 2, 3]"), + ":0:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las="}, + // Map in representation of an array + {util::JsonToVariant("{\"0\":1, \"1\":2, \"2\":3}"), + ":0:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las="}, + // Array more than 10 elements + {util::JsonToVariant("[7, 2, 3, 9, 5, 6, 1, 4, 8, 10, 11]"), + ":0:7wQgMram7RVqVIg/xRZWPfygGx0=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las=:3:M7Kyw8zsPkNHRw35uJ1vdPacr90=" + ":4:w28swksk9+tXf5jEdS9R5oSFAv8=:5:qb1N9GrUXfC3JyZPF8EXiNYcv4I=" + ":6:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:7:eVih19a6ZDz3NL32uVBtg9KSgQY=" + ":8:pITK737CVleu2Q4bHJTdQ4dJnCg=:9:+r5aI9HvKKagELki8SYKBk0q7D4=" + ":10:+aUUrIPmWZcSiV4ocCSLYRSFawE="}, + // Map in representation of an array more than 10 elements + {util::JsonToVariant( + "{\"0\":7, \"1\":2, \"2\":3, \"3\":9, \"4\":5, \"5\":6, \"6\":1, " + "\"7\":4, \"8\":8, \"9\":10, \"10\":11}"), + ":0:7wQgMram7RVqVIg/xRZWPfygGx0=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las=:3:M7Kyw8zsPkNHRw35uJ1vdPacr90=" + ":4:w28swksk9+tXf5jEdS9R5oSFAv8=:5:qb1N9GrUXfC3JyZPF8EXiNYcv4I=" + ":6:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:7:eVih19a6ZDz3NL32uVBtg9KSgQY=" + ":8:pITK737CVleu2Q4bHJTdQ4dJnCg=:9:+r5aI9HvKKagELki8SYKBk0q7D4=" + ":10:+aUUrIPmWZcSiV4ocCSLYRSFawE="}, + // Array with priority of different types + {util::JsonToVariant( + "[1,{\".value\":2,\".priority\":\"1\"},{\".value\":3,\".priority\":" + "1.1},{\".value\":4,\".priority\":1}]"), + ":0:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:3:MTfbusV7VkrLc1KUkR7t8903AO0=" + ":2:McRf84Bik6f4pUV86mpvDCk7CIY=:1:xJPtZCG4C1Z2dsXLdmD4nuEeJWg="}, + // Map with mixed numeric and alphanumeric keys + {util::JsonToVariant("{\"1\":10, \"01\":7, \"001\":8, \"10\":20, " + "\"11\":29, \"12\":25, \"A\":15}"), + ":1:+r5aI9HvKKagELki8SYKBk0q7D4=:01:7wQgMram7RVqVIg/xRZWPfygGx0=" + ":001:pITK737CVleu2Q4bHJTdQ4dJnCg=:10:KAU+hDgZHcHeW8Ejndss7NJXOts=" + ":11:6+iMnJRA9k8I9jMianUFkJUZ2as=:12:EBgCJ72ufYyBZo/vQcusywSQr0k=" + ":A:o0Z01FiFkcaCNvXrl/rO9/d+zjk="}, + // LeafNode with priority + {util::JsonToVariant("{\".value\":2,\".priority\":1.0}"), + "priority:number:3ff0000000000000:number:4000000000000000"}, + // Map with priority + {util::JsonToVariant("{\".priority\":2.0,\"A\":2}"), + "priority:number:4000000000000000::A:WtSt2Xo3L0JtPuArzQHofPrZOuU="}, + // Nested priority + {util::JsonToVariant( + "{\".priority\":3.0,\"A\":{\".value\":2,\".priority\":1.0}}"), + "priority:number:4008000000000000::A:iiz9CIvYWkKdETTpjVFBJNx1SiI="}, + }; + + std::string hash_rep; + for (const auto& test : test_cases) { + EXPECT_THAT(GetHashRepresentation(test.first, &hash_rep), Eq(test.second)); + } +} + +TEST(UtilDesktopTest, GetHash) { + std::vector> test_cases = { + // Null + {Variant::Null(), ""}, + // Int64 + {Variant(0), "7ysMph9WPitGP7poMnMHMVPtUlI="}, + {Variant(1), "YPVfR2bXt/lcDjiQZ8pOkAd3qkQ="}, + {Variant::FromInt64(INT64_MIN), "t8Zsu6QlM7Q4staTHVsgiTYxyUs="}, + // Double + {Variant(0.1), "wtQjBi5TBE+ZcdekL6INiSeCSQI="}, + {Variant(1.2345678901234567), "xy9cBNnU0nPSZZ/ZhBUrD5JZHqI="}, + {Variant(12345.678901234567), "dY5swb32BtBwcxLG0QSzKrxF4Ek="}, + {Variant(1234567890123456.5), "TnvxroHDDUski72FbjG9s1opR2U="}, + // Boolean + {Variant(true), "E5z61QM0lN/U2WsOnusszCTkR8M="}, + {Variant(false), "aSSNoqcS4oQwJ2xxH20rvpp3zP0="}, + // String + {Variant("i"), "DeH+bYeyNKPWpoASovNpeBOhCLU="}, + {Variant("ii"), "bzF9bn9qYLhJmuc33tDqMMVtgkY="}, + {Variant("iii"), "vHKAStiyuxaQKEElU3MxAxJ+Pjk="}, + {Variant("iiii"), "vX9ogm9I6wB/x0t3LY9jfsgwRhs="}, + // UTF-8 String + {Variant("αβγωΑΒΓΩ"), "7VgSkcL0RRqd5MecDe/uvdDP/LM="}, + // Basic Map + {util::JsonToVariant("{\"B2\":2,\"B1\":1}"), + "saXm0YMzvotwh2WvsZFatveeAZk="}, + // Map with priority + {util::JsonToVariant( + "{\"B1\":{\".value\":1,\".priority\":2.0},\"B2\":{\".value\":2," + "\".priority\":1.0},\"B3\":3}"), + "9q4+gOobE1ozTZyb85m/iDxoYzY="}, + // Array + {util::JsonToVariant("[1, 2, 3]"), "h6XOC3OcidJlNC1Velmi3gphgQk="}, + // Map in representation of an array. + {util::JsonToVariant("{\"0\":1, \"1\":2, \"2\":3}"), + "h6XOC3OcidJlNC1Velmi3gphgQk="}, + // Array more than 10 elements + {util::JsonToVariant("[7, 2, 3, 9, 5, 6, 1, 4, 8, 10, 11]"), + "0iPsE+86XkEMyhTUqK19iX0O+/E="}, + // Map in representation of an array more than 10 elements + {util::JsonToVariant( + "{\"0\":7, \"1\":2, \"2\":3, \"3\":9, \"4\":5, \"5\":6, \"6\":1, " + "\"7\":4, \"8\":8, \"9\":10, \"10\":11}"), + "0iPsE+86XkEMyhTUqK19iX0O+/E="}, + // Array with priority of different types + {util::JsonToVariant( + "[1,{\".value\":2,\".priority\":\"1\"},{\".value\":3,\".priority\":" + "1.1},{\".value\":4,\".priority\":1}]"), + "PfCbiYP2e75wAxeBx078Rpag/as="}, + // Map with mixed numeric and alphanumeric keys + {util::JsonToVariant("{\"1\":10, \"01\":7, \"001\":8, \"10\":20, " + "\"11\":29, \"12\":25, \"A\":15}"), + "fYENO1aD55oc6I6f+FM+cv1Y1yc="}, + // LeafNode with priority + {util::JsonToVariant("{\".value\":2,\".priority\":1.0}"), + "iiz9CIvYWkKdETTpjVFBJNx1SiI="}, + // Map with priority + {util::JsonToVariant("{\".priority\":2.0,\"A\":2}"), + "1xHri2Z3/K1NzjMObwiYwEfgo18="}, + // Nested priority + {util::JsonToVariant( + "{\".priority\":3.0,\"A\":{\".value\":2,\".priority\":1.0}}"), + "YpFTODg262pl4OnB8L9w0QdeZpM="}, + }; + + std::string hash; + for (const auto& test : test_cases) { + EXPECT_THAT(GetHash(test.first, &hash), Eq(test.second)); + } +} + +TEST(UtilDesktopTest, QuerySpecLoadsAllData) { + QuerySpec spec_default; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_default)); + + QuerySpec spec_order_by_key; + spec_order_by_key.params.order_by = QueryParams::kOrderByKey; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_order_by_key)); + + QuerySpec spec_order_by_value; + spec_order_by_value.params.order_by = QueryParams::kOrderByValue; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_order_by_value)); + + QuerySpec spec_order_by_child; + spec_order_by_child.params.order_by = QueryParams::kOrderByChild; + spec_order_by_child.params.order_by_child = "baby_mario"; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_order_by_child)); + + QuerySpec spec_start_at_value; + spec_start_at_value.params.start_at_value = 0; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_start_at_value)); + + QuerySpec spec_start_at_child_key; + spec_start_at_child_key.params.start_at_child_key = "a"; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_start_at_child_key)); + + QuerySpec spec_end_at_value; + spec_end_at_value.params.end_at_value = 9999; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_end_at_value)); + + QuerySpec spec_end_at_child_key; + spec_end_at_child_key.params.end_at_child_key = "z"; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_end_at_child_key)); + + QuerySpec spec_equal_to_value; + spec_equal_to_value.params.equal_to_value = 5000; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_equal_to_value)); + + QuerySpec spec_equal_to_child_key; + spec_equal_to_child_key.params.equal_to_child_key = "mn"; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_equal_to_child_key)); + + QuerySpec spec_limit_first; + spec_limit_first.params.limit_first = 10; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_limit_first)); + + QuerySpec spec_limit_last; + spec_limit_last.params.limit_last = 20; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_limit_last)); +} + +TEST(UtilDesktopTest, QuerySpecIsDefault) { + QuerySpec spec_default; + EXPECT_TRUE(QuerySpecIsDefault(spec_default)); + + QuerySpec spec_order_by_key; + spec_order_by_key.params.order_by = QueryParams::kOrderByKey; + EXPECT_FALSE(QuerySpecIsDefault(spec_order_by_key)); + + QuerySpec spec_order_by_value; + spec_order_by_value.params.order_by = QueryParams::kOrderByValue; + EXPECT_FALSE(QuerySpecIsDefault(spec_order_by_value)); + + QuerySpec spec_order_by_child; + spec_order_by_child.params.order_by = QueryParams::kOrderByChild; + spec_order_by_child.params.order_by_child = "baby_mario"; + EXPECT_FALSE(QuerySpecIsDefault(spec_order_by_child)); + + QuerySpec spec_start_at_value; + spec_start_at_value.params.start_at_value = 0; + EXPECT_FALSE(QuerySpecIsDefault(spec_start_at_value)); + + QuerySpec spec_start_at_child_key; + spec_start_at_child_key.params.start_at_child_key = "a"; + EXPECT_FALSE(QuerySpecIsDefault(spec_start_at_child_key)); + + QuerySpec spec_end_at_value; + spec_end_at_value.params.end_at_value = 9999; + EXPECT_FALSE(QuerySpecIsDefault(spec_end_at_value)); + + QuerySpec spec_end_at_child_key; + spec_end_at_child_key.params.end_at_child_key = "z"; + EXPECT_FALSE(QuerySpecIsDefault(spec_end_at_child_key)); + + QuerySpec spec_equal_to_value; + spec_equal_to_value.params.equal_to_value = 5000; + EXPECT_FALSE(QuerySpecIsDefault(spec_equal_to_value)); + + QuerySpec spec_equal_to_child_key; + spec_equal_to_child_key.params.equal_to_child_key = "mn"; + EXPECT_FALSE(QuerySpecIsDefault(spec_equal_to_child_key)); + + QuerySpec spec_limit_first; + spec_limit_first.params.limit_first = 10; + EXPECT_FALSE(QuerySpecIsDefault(spec_limit_first)); + + QuerySpec spec_limit_last; + spec_limit_last.params.limit_last = 20; + EXPECT_FALSE(QuerySpecIsDefault(spec_limit_last)); +} + +TEST(UtilDesktopTest, MakeDefaultQuerySpec) { + QuerySpec spec_default; + spec_default.path = Path("this/value/should/not/change"); + QuerySpec default_result = MakeDefaultQuerySpec(spec_default); + EXPECT_TRUE(QuerySpecIsDefault(default_result)); + EXPECT_EQ(default_result, spec_default); + + QuerySpec spec_featureful; + spec_featureful.path = Path("this/value/should/not/change"); + spec_featureful.params.order_by = QueryParams::kOrderByChild; + spec_featureful.params.order_by_child = "baby_mario"; + spec_featureful.params.start_at_value = 0; + spec_featureful.params.start_at_child_key = "a"; + spec_featureful.params.end_at_value = 9999; + spec_featureful.params.end_at_child_key = "z"; + spec_featureful.params.limit_first = 10; + spec_featureful.params.limit_last = 20; + QuerySpec featureful_result = MakeDefaultQuerySpec(spec_featureful); + EXPECT_TRUE(QuerySpecIsDefault(featureful_result)); + EXPECT_EQ(featureful_result, spec_default); +} + +TEST(UtilDesktopTest, WireProtocolPathToString) { + EXPECT_EQ(WireProtocolPathToString(Path()), "/"); + EXPECT_EQ(WireProtocolPathToString(Path("")), "/"); + EXPECT_EQ(WireProtocolPathToString(Path("/")), "/"); + EXPECT_EQ(WireProtocolPathToString(Path("///")), "/"); + + EXPECT_EQ(WireProtocolPathToString(Path("A")), "A"); + EXPECT_EQ(WireProtocolPathToString(Path("/A")), "A"); + EXPECT_EQ(WireProtocolPathToString(Path("A/")), "A"); + EXPECT_EQ(WireProtocolPathToString(Path("/A/")), "A"); + + EXPECT_EQ(WireProtocolPathToString(Path("A/B")), "A/B"); + EXPECT_EQ(WireProtocolPathToString(Path("/A/B")), "A/B"); + EXPECT_EQ(WireProtocolPathToString(Path("A/B/")), "A/B"); + EXPECT_EQ(WireProtocolPathToString(Path("/A/B/")), "A/B"); +} + +TEST(UtilDesktopTest, GetWireProtocolParams) { + { + QueryParams params_default; + EXPECT_EQ(GetWireProtocolParams(params_default), Variant::EmptyMap()); + } + + { + QueryParams params; + params.start_at_value = "0"; + + Variant expected(std::map{ + {"sp", "0"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.start_at_value = 0; + params.start_at_child_key = "0010"; + + Variant expected(std::map{ + {"sp", 0}, + {"sn", "0010"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.end_at_value = "0"; + + Variant expected(std::map{ + {"ep", "0"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.end_at_value = 0; + params.end_at_child_key = "0010"; + + Variant expected(std::map{ + {"ep", 0}, + {"en", "0010"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.equal_to_value = 3.14; + + Variant expected(std::map{ + {"sp", 3.14}, + {"ep", 3.14}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.equal_to_value = 3.14; + params.equal_to_child_key = "A"; + + Variant expected(std::map{ + {"sp", 3.14}, + {"sn", "A"}, + {"ep", 3.14}, + {"en", "A"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.limit_first = 10; + + Variant expected(std::map{ + {"l", 10}, + {"vf", "l"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.limit_last = 20; + + Variant expected(std::map{ + {"l", 20}, + {"vf", "r"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "A"; + + Variant expected(std::map{ + {"i", ".key"}, + {"sp", "A"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.end_at_value = "Z"; + + Variant expected(std::map{ + {"i", ".value"}, + {"ep", "Z"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = ""; + params.limit_first = 10; + + Variant expected(std::map{ + {"i", "/"}, + {"l", 10}, + {"vf", "l"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "/A/B/C/"; + params.limit_last = 20; + + Variant expected(std::map{ + {"i", "A/B/C"}, + {"l", 20}, + {"vf", "r"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } +} + +TEST(UtilDesktopTest, TestGetAppDataPath) { + // Make sure we get a path string. + EXPECT_NE(GetAppDataPath("testapp0"), ""); + + // Make sure we get 2 different paths for 2 different apps. + EXPECT_NE(GetAppDataPath("testapp1"), GetAppDataPath("testapp2")); + + // Make sure we get the same path if we are calling twice with the same app. + EXPECT_EQ(GetAppDataPath("testapp3"), GetAppDataPath("testapp3")); + + // Make sure the path string refers to a directory that is available. + std::string path = GetAppDataPath("testapp4", true); + struct stat s; + ASSERT_EQ(stat(path.c_str(), &s), 0) + << "stat failed on '" << path << "': " << strerror(errno); + EXPECT_TRUE(s.st_mode & S_IFDIR) << path << " is not a directory!"; + + // Write random data to a randomly generated filename. + std::string test_data = + std::string("Hello, world! ") + std::to_string(rand()); // NOLINT + std::string test_path = path + kPathSep + "test_file_" + + std::to_string(rand()) + ".txt"; // NOLINT + + // Ensure that we can save files in this directory. + FILE* out = fopen(test_path.c_str(), "w"); + EXPECT_NE(out, nullptr) << "Couldn't open test file for writing: " + << strerror(errno); + EXPECT_GE(fputs(test_data.c_str(), out), 0) << strerror(errno); + EXPECT_EQ(fclose(out), 0) << strerror(errno); + + FILE* in = fopen(test_path.c_str(), "r"); + EXPECT_NE(in, nullptr) << "Couldn't open test file for reading: " + << strerror(errno); + char buf[256]; + EXPECT_NE(fgets(buf, sizeof(buf), in), nullptr) << strerror(errno); + EXPECT_STREQ(buf, test_data.c_str()); + EXPECT_EQ(fclose(in), 0) << strerror(errno); + + // Delete the file. + EXPECT_EQ(unlink(test_path.c_str()), 0) << strerror(errno); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/change_test.cc b/database/tests/desktop/view/change_test.cc new file mode 100644 index 0000000000..e76d466b08 --- /dev/null +++ b/database/tests/desktop/view/change_test.cc @@ -0,0 +1,341 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/change.h" + +#include "app/src/include/firebase/variant.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(Change, DefaultConstructor) { + Change change; + EXPECT_EQ(change.indexed_variant.variant(), Variant::Null()); + EXPECT_EQ(change.child_key, ""); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, CopyConstructor) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("string variant")); + change.child_key = "Hello"; + change.prev_name = "World"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change copy_constructed(change); + EXPECT_EQ(copy_constructed.event_type, kEventTypeValue); + EXPECT_EQ(copy_constructed.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(copy_constructed.child_key, "Hello"); + EXPECT_EQ(copy_constructed.prev_name, "World"); + EXPECT_EQ(copy_constructed.old_indexed_variant.variant(), + Variant(1234567890)); + + Change copy_assigned; + copy_assigned = change; + EXPECT_EQ(copy_assigned.event_type, kEventTypeValue); + EXPECT_EQ(copy_assigned.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(copy_assigned.child_key, "Hello"); + EXPECT_EQ(copy_assigned.prev_name, "World"); + EXPECT_EQ(copy_assigned.old_indexed_variant.variant(), Variant(1234567890)); +} + +TEST(Change, MoveConstructor) { + { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("string variant")); + change.child_key = "Hello"; + change.prev_name = "World"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change move_constructed(std::move(change)); + EXPECT_EQ(move_constructed.event_type, kEventTypeValue); + EXPECT_EQ(move_constructed.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(move_constructed.child_key, "Hello"); + EXPECT_EQ(move_constructed.prev_name, "World"); + EXPECT_EQ(move_constructed.old_indexed_variant.variant(), + Variant(1234567890)); + } + + { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("string variant")); + change.child_key = "Hello"; + change.prev_name = "World"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change move_assigned; + move_assigned = change; + EXPECT_EQ(move_assigned.event_type, kEventTypeValue); + EXPECT_EQ(move_assigned.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(move_assigned.child_key, "Hello"); + EXPECT_EQ(move_assigned.prev_name, "World"); + EXPECT_EQ(move_assigned.old_indexed_variant.variant(), Variant(1234567890)); + } +} + +TEST(Change, Constructors) { + Change type_variant(kEventTypeValue, + IndexedVariant(Variant("abcdefghijklmnopqrstuvwxyz"))); + + EXPECT_EQ(type_variant.event_type, kEventTypeValue); + EXPECT_EQ(type_variant.indexed_variant.variant(), + Variant("abcdefghijklmnopqrstuvwxyz")); + EXPECT_EQ(type_variant.child_key, ""); + EXPECT_EQ(type_variant.prev_name, ""); + EXPECT_EQ(type_variant.old_indexed_variant.variant(), Variant::Null()); + + Change type_variant_string( + kEventTypeChildChanged, + IndexedVariant(Variant("zyxwvutsrqponmlkjihgfedcba")), "child_key"); + EXPECT_EQ(type_variant_string.event_type, kEventTypeChildChanged); + EXPECT_EQ(type_variant_string.indexed_variant.variant(), + Variant("zyxwvutsrqponmlkjihgfedcba")); + EXPECT_EQ(type_variant_string.child_key, "child_key"); + EXPECT_EQ(type_variant_string.prev_name, ""); + EXPECT_EQ(type_variant_string.old_indexed_variant.variant(), Variant::Null()); + + Change all_values_set(kEventTypeChildRemoved, + IndexedVariant(Variant("ABCDEFGHIJKLMNOPQRSTUVWXYZ")), + "another_child_key", "previous_child", + IndexedVariant(Variant("ZYXWVUSTRQPONMLKJIHGFEDCBA"))); + EXPECT_EQ(all_values_set.event_type, kEventTypeChildRemoved); + EXPECT_EQ(all_values_set.indexed_variant.variant(), + Variant("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + EXPECT_EQ(all_values_set.child_key, "another_child_key"); + EXPECT_EQ(all_values_set.prev_name, "previous_child"); + EXPECT_EQ(all_values_set.old_indexed_variant.variant(), + Variant("ZYXWVUSTRQPONMLKJIHGFEDCBA")); +} + +TEST(Change, ValueChange) { + Change change = ValueChange(IndexedVariant(Variant("ValueChanged!"))); + + EXPECT_EQ(change.event_type, kEventTypeValue); + EXPECT_EQ(change.indexed_variant.variant(), + IndexedVariant(Variant("ValueChanged!")).variant()); + EXPECT_EQ(change.child_key, ""); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChildAddedChange) { + Change change = + ChildAddedChange("child_key", IndexedVariant(Variant("ValueChanged!"))); + + EXPECT_EQ(change.event_type, kEventTypeChildAdded); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ValueChanged!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); + + Change another_change = + ChildAddedChange("another_child_key", Variant("!ChangedValue")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildAdded); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!ChangedValue")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChildRemovedChange) { + Change change = + ChildRemovedChange("child_key", IndexedVariant(Variant("ChildRemoved!"))); + + EXPECT_EQ(change.event_type, kEventTypeChildRemoved); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ChildRemoved!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); + + Change another_change = + ChildRemovedChange("another_child_key", Variant("!RemovedChild")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildRemoved); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!RemovedChild")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChildChangedChange) { + Change change = + ChildChangedChange("child_key", IndexedVariant(Variant("ChildChanged!")), + IndexedVariant(Variant("old value"))); + + EXPECT_EQ(change.event_type, kEventTypeChildChanged); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ChildChanged!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant("old value")); + + Change another_change = ChildChangedChange( + "another_child_key", Variant("!ChangedChild"), Variant("previous value")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildChanged); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!ChangedChild")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), + Variant("previous value")); +} + +TEST(Change, ChildMovedChange) { + Change change = + ChildMovedChange("child_key", IndexedVariant(Variant("ChildChanged!"))); + + EXPECT_EQ(change.event_type, kEventTypeChildMoved); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ChildChanged!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); + + Change another_change = + ChildMovedChange("another_child_key", Variant("!ChangedChild")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildMoved); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!ChangedChild")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChangeWithPrevName) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("value")); + change.child_key = "child_key"; + change.prev_name = ""; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change result = ChangeWithPrevName(change, "prev_name"); + + EXPECT_EQ(result.event_type, kEventTypeValue); + EXPECT_EQ(result.indexed_variant.variant(), + IndexedVariant("value").variant()); + EXPECT_EQ(result.child_key, "child_key"); + EXPECT_EQ(result.prev_name, "prev_name"); + EXPECT_EQ(result.old_indexed_variant.variant(), Variant(1234567890)); +} + +TEST(Change, EqualityOperatorSame) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("value")); + change.child_key = "child_key"; + change.prev_name = "prev_name"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change identical_change; + identical_change.event_type = kEventTypeValue; + identical_change.indexed_variant = IndexedVariant(Variant("value")); + identical_change.child_key = "child_key"; + identical_change.prev_name = "prev_name"; + identical_change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + // Verify the == and != operators return the expected result. + // Check equality with self. + EXPECT_TRUE(change == change); + EXPECT_FALSE(change != change); + + // Check equality with an identical change. + EXPECT_TRUE(change == identical_change); + EXPECT_FALSE(change != identical_change); +} + +TEST(Change, EqualityOperatorDifferent) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("value")); + change.child_key = "child_key"; + change.prev_name = "prev_name"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change change_different_type; + change_different_type.event_type = kEventTypeChildAdded; + change_different_type.indexed_variant = IndexedVariant(Variant("value")); + change_different_type.child_key = "child_key"; + change_different_type.prev_name = "prev_name"; + change_different_type.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_indexed_variant; + change_different_indexed_variant.event_type = kEventTypeValue; + change_different_indexed_variant.indexed_variant = + IndexedVariant(Variant("aeluv")); + change_different_indexed_variant.child_key = "child_key"; + change_different_indexed_variant.prev_name = "prev_name"; + change_different_indexed_variant.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_child_key; + change_different_child_key.event_type = kEventTypeValue; + change_different_child_key.indexed_variant = IndexedVariant(Variant("value")); + change_different_child_key.child_key = "cousin_key"; + change_different_child_key.prev_name = "prev_name"; + change_different_child_key.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_prev_name; + change_different_prev_name.event_type = kEventTypeValue; + change_different_prev_name.indexed_variant = IndexedVariant(Variant("value")); + change_different_prev_name.child_key = "child_key"; + change_different_prev_name.prev_name = "next_name"; + change_different_prev_name.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_old_indexed_variant; + change_different_old_indexed_variant.event_type = kEventTypeValue; + change_different_old_indexed_variant.indexed_variant = + IndexedVariant(Variant("value")); + change_different_old_indexed_variant.child_key = "child_key"; + change_different_old_indexed_variant.prev_name = "prev_name"; + change_different_old_indexed_variant.old_indexed_variant = + IndexedVariant(Variant(int64_t(9876543210))); + + // Verify the == and != operators return the expected result. + EXPECT_FALSE(change == change_different_type); + EXPECT_TRUE(change != change_different_type); + + EXPECT_FALSE(change == change_different_indexed_variant); + EXPECT_TRUE(change != change_different_indexed_variant); + + EXPECT_FALSE(change == change_different_child_key); + EXPECT_TRUE(change != change_different_child_key); + + EXPECT_FALSE(change == change_different_prev_name); + EXPECT_TRUE(change != change_different_prev_name); + + EXPECT_FALSE(change == change_different_old_indexed_variant); + EXPECT_TRUE(change != change_different_old_indexed_variant); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/child_change_accumulator_test.cc b/database/tests/desktop/view/child_change_accumulator_test.cc new file mode 100644 index 0000000000..78333b252f --- /dev/null +++ b/database/tests/desktop/view/child_change_accumulator_test.cc @@ -0,0 +1,182 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/child_change_accumulator.h" +#include "database/src/desktop/view/change.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// Test to add new change data to the accumulator. +TEST(ChildChangeAccumulator, TrackChildChangeNew) { + // Add single ChildAdded change to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change = ChildAddedChange("ChildAdd", 1); + TrackChildChange(change, &accumulator); + + auto it = accumulator.find("ChildAdd"); + ASSERT_NE(it, accumulator.end()); + + EXPECT_EQ(it->second, change); + } + // Add single ChildChanged change to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change = ChildChangedChange("ChildChange", "new", "old"); + TrackChildChange(change, &accumulator); + + auto it = accumulator.find("ChildChange"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, change); + } + // Add single ChildRemoved change to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change = ChildRemovedChange("ChildRemove", true); + TrackChildChange(change, &accumulator); + + auto it = accumulator.find("ChildRemove"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, change); + } + // Add all ChildAdded, ChildChanged, ChildRemoved change with different child + // key to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change_add = ChildAddedChange("ChildAdd", 1); + TrackChildChange(change_add, &accumulator); + + Change change_change = ChildChangedChange("ChildChange", "new", "old"); + TrackChildChange(change_change, &accumulator); + + Change change_remove = ChildRemovedChange("ChildRemove", true); + TrackChildChange(change_remove, &accumulator); + + // Verify child "ChildAdd" + auto it_add = accumulator.find("ChildAdd"); + ASSERT_NE(it_add, accumulator.end()); + EXPECT_EQ(it_add->second, change_add); + + // Verify child "ChildChange" + auto it_change = accumulator.find("ChildChange"); + ASSERT_NE(it_change, accumulator.end()); + EXPECT_EQ(it_change->second, change_change); + + // Verify child "ChildRemove" + auto it_remove = accumulator.find("ChildRemove"); + ASSERT_NE(it_remove, accumulator.end()); + EXPECT_EQ(it_remove->second, change_remove); + } +} + +// Test to resolve ChildRemoved change and ChildAdded change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeRemovedThenAdded) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildRemovedChange("ChildRemoveThenAdd", "old"), + &accumulator); + TrackChildChange(ChildAddedChange("ChildRemoveThenAdd", "new"), &accumulator); + + // Expected result should be a ChildChanged change from "old" to "new" + Change expected = ChildChangedChange("ChildRemoveThenAdd", "new", "old"); + + auto it = accumulator.find("ChildRemoveThenAdd"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +// Test to resolve ChildAdded change and ChildRemoved change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeAddedThenRemoved) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildAddedChange("ChildAddThenRemove", 1), &accumulator); + // Note: the removed value "true" does not need to match the value "1" added + // previously. + TrackChildChange(ChildRemovedChange("ChildAddThenRemove", true), + &accumulator); + + // Expect the child data to be removed + auto it = accumulator.find("ChildAddAndRemove"); + ASSERT_EQ(it, accumulator.end()); +} + +// Test to resolve ChildChanged change and ChildRemoved change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeChangedThenRemoved) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildChangedChange("ChildChangeThenRemove", "old", "order"), + &accumulator); + // Note: the removed value "new" does not need to match the value "old" + // changed previously. + TrackChildChange(ChildRemovedChange("ChildChangeThenRemove", "new"), + &accumulator); + + // Expected result should be a ChildRemoved change from "old" value + Change expected = ChildRemovedChange("ChildChangeThenRemove", "old"); + + auto it = accumulator.find("ChildChangeThenRemove"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +// Test to resolve ChildAdded change and ChildChanged change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeAddedThenChanged) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildAddedChange("ChildAddThenChange", "old"), &accumulator); + // Note: the old value "something else" does not need to match the value "old" + // added previously. + TrackChildChange( + ChildChangedChange("ChildAddThenChange", "new", "something else"), + &accumulator); + + // Expected result should be a ChildAdded change with "new" value + Change expected = ChildAddedChange("ChildAddThenChange", "new"); + + auto it = accumulator.find("ChildAddThenChange"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +// Test to resolve ChildChanged change and ChildChanged change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeChangedThenChanged) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildChangedChange("ChildChangeThenChange", "old", "older"), + &accumulator); + // Note: the old value "something else" does not need to match the value "old" + // changed previously. + TrackChildChange( + ChildChangedChange("ChildChangeThenChange", "new", "something else"), + &accumulator); + + // Expected result should be a ChildChanged change from "older" to "new". + Change expected = ChildChangedChange("ChildChangeThenChange", "new", "older"); + + auto it = accumulator.find("ChildChangeThenChange"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/event_generator_test.cc b/database/tests/desktop/view/event_generator_test.cc new file mode 100644 index 0000000000..5ebc3e40a2 --- /dev/null +++ b/database/tests/desktop/view/event_generator_test.cc @@ -0,0 +1,346 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/event_generator.h" + +#include + +#include "app/src/include/firebase/variant.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/util_desktop.h" + +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { + +class EventGeneratorTest : public testing::Test { + public: + void SetUp() override { + query_spec_.path = Path("prefix/path"); + data_cache_ = Variant(std::map{ + std::make_pair("aaa", CombineValueAndPriority(100, 1)), + std::make_pair("bbb", CombineValueAndPriority(200, 2)), + std::make_pair("ccc", CombineValueAndPriority(300, 3)), + std::make_pair("ddd", CombineValueAndPriority(400, 4)), + }); + event_cache_ = IndexedVariant(data_cache_, query_spec_.params); + value_registration_ = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + child_registration_ = + new ChildEventRegistration(nullptr, nullptr, QuerySpec()); + event_registrations_ = std::vector>{ + UniquePtr(value_registration_), + UniquePtr(child_registration_), + }; + } + + protected: + QuerySpec query_spec_; + Variant data_cache_; + IndexedVariant event_cache_; + ValueEventRegistration* value_registration_; + ChildEventRegistration* child_registration_; + std::vector> event_registrations_; +}; + +class EventGeneratorDeathTest : public EventGeneratorTest {}; + +TEST_F(EventGeneratorTest, GenerateEventsForChangesAllAdded) { + std::vector changes{ + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("ccc", CombineValueAndPriority(300, 3)), + ChildAddedChange("ddd", CombineValueAndPriority(400, 4)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + std::vector expected{ + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesAllAddedReverseOrder) { + std::vector changes{ + ChildAddedChange("ddd", CombineValueAndPriority(400, 4)), + ChildAddedChange("ccc", CombineValueAndPriority(300, 3)), + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the query_spec's comparison + // rules. In this case, based on priority. + std::vector expected{ + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesDifferentTypes) { + std::vector changes{ + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 3)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the EventType. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesSomeDifferentTypes) { + std::vector changes{ + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildChangedChange("ddd", CombineValueAndPriority(400, 4), + CombineValueAndPriority(400, 4)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 3)), + ChildRemovedChange("fff", CombineValueAndPriority(600, 6)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the EventType and the + // query_spec's comparison rules. In this case, based on priority. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(600, 6), + QuerySpec(Path("prefix/path/fff"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesWithDifferentPriorities) { + std::vector changes{ + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + // The priorities of ccc and ddd are reversed in the old snapshot. + ChildChangedChange("ddd", CombineValueAndPriority(400, 4), + CombineValueAndPriority(400, 3)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 4)), + ChildRemovedChange("fff", CombineValueAndPriority(600, 6)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the EventType and the + // query_spec's comparison rules. In this case, based on priority. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(600, 6), + QuerySpec(Path("prefix/path/fff"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + // Moving the priority generated both move and change events. + Event(kEventTypeChildMoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildMoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesWithDifferentQuerySpec) { + std::vector changes{ + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildChangedChange("ddd", CombineValueAndPriority(400, 4), + CombineValueAndPriority(400, 3)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 4)), + ChildRemovedChange("fff", CombineValueAndPriority(600, 6)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + // Changing the priority doesn't matter when the QuerySpec does not consider + // priority (e.g., when it orders the elements by value). + QuerySpec value_query_spec = query_spec_; + value_query_spec.params.order_by = QueryParams::kOrderByValue; + + std::vector result = GenerateEventsForChanges( + value_query_spec, changes, event_cache_, event_registrations_); + + // No move events this time around even though the priorities changed because + // the QuerySpec isn't ordered by priority, it's ordered by value. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(600, 6), + QuerySpec(Path("prefix/path/fff"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorDeathTest, MissingChildName) { + std::vector changes{ + ChildAddedChange("", CombineValueAndPriority(100, 1)), + }; + // All child changes are expected to have a key. Missing a key means we have a + // malformed Change object. + EXPECT_DEATH(GenerateEventsForChanges(QuerySpec(), changes, event_cache_, + event_registrations_), + DEATHTEST_SIGABRT); +} + +TEST_F(EventGeneratorDeathTest, MultipleValueChanges) { + std::vector changes{ + ValueChange(IndexedVariant(Variant("aaa"))), + ValueChange(IndexedVariant(Variant("bbb"))), + }; + // Value changes only occur one at a time, so if we have two something has + // gone wrong at the call site. + EXPECT_DEATH(GenerateEventsForChanges(QuerySpec(), changes, event_cache_, + event_registrations_), + DEATHTEST_SIGABRT); +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/indexed_filter_test.cc b/database/tests/desktop/view/indexed_filter_test.cc new file mode 100644 index 0000000000..96a4d9ce7a --- /dev/null +++ b/database/tests/desktop/view/indexed_filter_test.cc @@ -0,0 +1,391 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/indexed_filter.h" +#include "app/src/variant_util.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/indexed_variant.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(IndexedFilter, UpdateChild_SameValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("aaa"); + Variant new_child(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }); + Path affected_path("bbb/ccc"); + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(old_child); + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + // Expect no changes + EXPECT_EQ(change_accumulator, ChildChangeAccumulator()); +} + +TEST(IndexedFilter, UpdateChild_ChangedValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("aaa"); + Variant new_child(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + }), + }); + Path affected_path("bbb/ccc"); + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(std::map{ + std::make_pair("aaa", new_child), + }); + ChildChangeAccumulator expected_changes{ + std::make_pair( + "aaa", + ChildChangedChange("aaa", new_child, + Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }))), + }; + + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + EXPECT_EQ(change_accumulator, expected_changes); +} + +TEST(IndexedFilter, UpdateChild_AddedValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("ddd"); + Variant new_child(std::map{ + std::make_pair("eee", 200), + }); + Path affected_path; + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + std::make_pair("ddd", + std::map{ + std::make_pair("eee", 200), + }), + }); + ChildChangeAccumulator expected_changes{ + std::make_pair("ddd", ChildAddedChange("ddd", new_child)), + }; + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); +} + +TEST(IndexedFilter, UpdateChild_RemovedValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("aaa"); + Variant new_child = Variant::Null(); + Path affected_path; + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(Variant::EmptyMap()); + ChildChangeAccumulator expected_changes{ + std::make_pair( + "aaa", + ChildRemovedChange("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + })), + }; + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); +} + +TEST(IndexedFilterDeathTest, UpdateChild_OrderByMismatch) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + IndexedFilter filter(params); + + IndexedVariant good_snap(Variant(), params); + IndexedVariant bad_snap; + + // Should be fine. + filter.UpdateChild(good_snap, "irrelevant_key", Variant("irrelevant variant"), + Path("irrelevant/path"), nullptr, nullptr); + + // Should die. + EXPECT_DEATH(filter.UpdateChild(bad_snap, "irrelevant_key", + Variant("irrelevant variant"), + Path("irrelevant/path"), nullptr, nullptr), + DEATHTEST_SIGABRT); +} + +TEST(IndexedFilter, UpdateFullVariant) { + { + QueryParams params; + IndexedFilter filter(params); + + IndexedVariant old_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 100), + std::make_pair("to_be_removed", 200), + std::make_pair("unchanged", 300), + }), + })); + IndexedVariant new_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 400), + std::make_pair("unchanged", 300), + std::make_pair("was_added", 500), + }), + })); + ChildChangeAccumulator change_accumulator; + + ChildChangeAccumulator expected_changes{ + std::make_pair("to_be_changed", + ChildChangedChange("to_be_changed", 400, 100)), + std::make_pair("to_be_removed", + ChildRemovedChange("to_be_removed", 200)), + std::make_pair("was_added", ChildAddedChange("was_added", 500)), + }; + + EXPECT_EQ(filter.UpdateFullVariant(old_snap, new_snap, &change_accumulator), + new_snap); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); + } + { + QueryParams params; + IndexedFilter filter(params); + + IndexedVariant old_snap(Variant(std::map{ + std::make_pair("to_be_changed", 100), + std::make_pair("to_be_removed", 200), + std::make_pair("unchanged", 300), + })); + IndexedVariant new_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 400), + std::make_pair("unchanged", 300), + std::make_pair("was_added", 500), + }), + })); + ChildChangeAccumulator change_accumulator; + + ChildChangeAccumulator expected_changes{ + std::make_pair("to_be_changed", + ChildChangedChange("to_be_changed", 400, 100)), + std::make_pair("to_be_removed", + ChildRemovedChange("to_be_removed", 200)), + std::make_pair("was_added", ChildAddedChange("was_added", 500)), + }; + + EXPECT_EQ(filter.UpdateFullVariant(old_snap, new_snap, &change_accumulator), + new_snap); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); + } + { + QueryParams params; + IndexedFilter filter(params); + + IndexedVariant old_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 100), + std::make_pair("to_be_removed", 200), + std::make_pair("unchanged", 300), + }), + })); + IndexedVariant new_snap(Variant(std::map{ + std::make_pair("to_be_changed", 400), + std::make_pair("unchanged", 300), + std::make_pair("was_added", 500), + })); + ChildChangeAccumulator change_accumulator; + + ChildChangeAccumulator expected_changes{ + std::make_pair("to_be_changed", + ChildChangedChange("to_be_changed", 400, 100)), + std::make_pair("to_be_removed", + ChildRemovedChange("to_be_removed", 200)), + std::make_pair("was_added", ChildAddedChange("was_added", 500)), + }; + + EXPECT_EQ(filter.UpdateFullVariant(old_snap, new_snap, &change_accumulator), + new_snap); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); + } +} + +TEST(IndexedFilterDeathTest, UpdateFullVariant_OrderByMismatch) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + IndexedFilter filter(params); + + IndexedVariant irrelevant_snap; + IndexedVariant good_new_snap(Variant(), params); + IndexedVariant bad_new_snap; + + // Should not die. + filter.UpdateFullVariant(irrelevant_snap, good_new_snap, nullptr); + + // Should die. + EXPECT_DEATH(filter.UpdateFullVariant(irrelevant_snap, bad_new_snap, nullptr), + DEATHTEST_SIGABRT); +} + +TEST(IndexedFilter, UpdatePriority_Null) { + QueryParams params; + IndexedFilter filter(params); + IndexedVariant old_snap(Variant::Null()); + Variant new_priority = 100; + IndexedVariant result = filter.UpdatePriority(old_snap, new_priority); + EXPECT_EQ(result.variant(), Variant::Null()); +} + +TEST(IndexedFilter, UpdatePriority_FundamentalType) { + QueryParams params; + IndexedFilter filter(params); + IndexedVariant old_snap(Variant(100)); + Variant new_priority = "priority"; + IndexedVariant result = filter.UpdatePriority(old_snap, new_priority); + EXPECT_EQ(result.variant(), Variant(std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", "priority"), + })); +} + +TEST(IndexedFilter, UpdatePriority_Map) { + QueryParams params; + IndexedFilter filter(params); + IndexedVariant old_snap(Variant(std::map{ + std::make_pair("aaa", 111), + std::make_pair("bbb", 222), + std::make_pair("ccc", 333), + })); + Variant new_priority = "banana"; + IndexedVariant result = filter.UpdatePriority(old_snap, new_priority); + EXPECT_EQ(result.variant(), Variant(std::map{ + std::make_pair("aaa", 111), + std::make_pair("bbb", 222), + std::make_pair("ccc", 333), + std::make_pair(".priority", "banana"), + })); +} + +TEST(IndexedFilter, FiltersVariants) { + QueryParams params; + IndexedFilter filter(params); + EXPECT_FALSE(filter.FiltersVariants()); +} + +TEST(IndexedFilter, GetIndexedFilter) { + QueryParams params; + IndexedFilter filter(params); + EXPECT_EQ(filter.GetIndexedFilter(), &filter); +} + +TEST(IndexedFilter, query_spec) { + QueryParams params; + IndexedFilter filter(params); + EXPECT_EQ(filter.query_params(), params); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/limited_filter_test.cc b/database/tests/desktop/view/limited_filter_test.cc new file mode 100644 index 0000000000..ba81aae08a --- /dev/null +++ b/database/tests/desktop/view/limited_filter_test.cc @@ -0,0 +1,314 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/view/limited_filter.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(LimitedFilter, Constructor) { + { + QueryParams params; + params.limit_first = 2; + LimitedFilter filter(params); + } + { + QueryParams params; + params.limit_last = 2; + LimitedFilter filter(params); + } +} + +TEST(LimitedFilter, UpdateChildLimitFirst) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_first = 2; + LimitedFilter filter(params); + + Variant data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant old_snapshot(data, params); + + // Prepend new value. + { + IndexedVariant changed_result = + filter.UpdateChild(old_snapshot, "aaa", 100, Path(), nullptr, nullptr); + Variant expected_data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + }; + IndexedVariant expected_changed_result(expected_data, params); + EXPECT_EQ(changed_result, expected_changed_result); + } + + // New value at the end doesn't get appended. + { + IndexedVariant unchanged_result = + filter.UpdateChild(old_snapshot, "ddd", 400, Path(), nullptr, nullptr); + IndexedVariant expected_unchanged_result(data, params); + EXPECT_EQ(unchanged_result, expected_unchanged_result); + } +} + +TEST(LimitedFilter, UpdateChildLimitLast) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_last = 2; + LimitedFilter filter(params); + + Variant data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant old_snapshot(data, params); + + // New value at the beginning doesn't get prepending. + { + IndexedVariant unchanged_result = + filter.UpdateChild(old_snapshot, "aaa", 100, Path(), nullptr, nullptr); + IndexedVariant expected_unchanged_result(data, params); + EXPECT_EQ(unchanged_result, expected_unchanged_result); + } + + // Append new value. + { + IndexedVariant changed_result = + filter.UpdateChild(old_snapshot, "ddd", 400, Path(), nullptr, nullptr); + Variant expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_changed_result(expected_data, params); + EXPECT_EQ(changed_result, expected_changed_result); + } +} + +TEST(LimitedFilter, UpdateFullVariantLimitFirst) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_first = 2; + LimitedFilter filter(params); + + Variant old_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(old_data, params); + + // new_data removes elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data removes elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), std::make_pair("ccc", 300), + std::make_pair("ddd", 400), std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } +} + +TEST(LimitedFilter, UpdateFullVariantLimitLast) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_last = 2; + LimitedFilter filter(params); + + Variant old_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(old_data, params); + + // new_data removes elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data removes elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), std::make_pair("ccc", 300), + std::make_pair("ddd", 400), std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } +} + +TEST(LimitedFilter, UpdatePriority) { + QueryParams params; + params.limit_last = 2; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant priority = 9999; + IndexedVariant old_snapshot(data, params); + + // Same as old_snapshot. + IndexedVariant expected_value(data, params); + EXPECT_EQ(filter.UpdatePriority(old_snapshot, priority), expected_value); +} + +TEST(LimitedFilter, FiltersVariants) { + QueryParams params; + params.limit_last = 2; + LimitedFilter filter(params); + EXPECT_TRUE(filter.FiltersVariants()); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/ranged_filter_test.cc b/database/tests/desktop/view/ranged_filter_test.cc new file mode 100644 index 0000000000..174d710015 --- /dev/null +++ b/database/tests/desktop/view/ranged_filter_test.cc @@ -0,0 +1,673 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/view/ranged_filter.h" + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(RangedFilter, Constructor) { + // Test the assert condition in the RangedFilter. The filter must have one of + // the parameters set that affects the range of the query. + { + QueryParams params; + params.start_at_child_key = "the_beginning"; + RangedFilter filter(params); + } + { + QueryParams params; + params.start_at_value = Variant("the_beginning_value"); + RangedFilter filter(params); + } + { + QueryParams params; + params.end_at_child_key = "the_end"; + RangedFilter filter(params); + } + { + QueryParams params; + params.end_at_value = Variant("fin"); + RangedFilter filter(params); + } + { + QueryParams params; + params.equal_to_child_key = "specific_key"; + RangedFilter filter(params); + } + { + QueryParams params; + params.equal_to_value = Variant("specific_value"); + RangedFilter filter(params); + } +} + +TEST(RangedFilter, UpdateChildWithChildKeyFilter) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "ccc"; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(data, params); + + // Add a new value that is outside of the range, which should not change + // the result. + IndexedVariant result = + filter.UpdateChild(old_snapshot, "aaa", 100, Path(), nullptr, nullptr); + + IndexedVariant expected_result(data, params); + EXPECT_EQ(result, expected_result); + + // Now add a new value that is inside the allowed range, and the result + // should update. + IndexedVariant new_result = + filter.UpdateChild(old_snapshot, "fff", 600, Path(), nullptr, nullptr); + + Variant new_expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant new_expected_result(new_expected_data, params); + + EXPECT_EQ(new_result, new_expected_result); +} + +TEST(RangedFilter, UpdateFullVariant) { + // Leaf + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "bbb"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + IndexedVariant old_snapshot(Variant::EmptyMap(), params); + IndexedVariant new_snapshot(1000, params); + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + EXPECT_EQ(result, IndexedVariant(Variant::Null(), params)); + } + + // Map + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "bbb"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(Variant::EmptyMap(), params); + IndexedVariant new_snapshot(data, params); + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + + Variant expected_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_result(expected_data, params); + + EXPECT_EQ(result, expected_result); + } +} + +TEST(RangedFilter, UpdatePriority) { + QueryParams params; + params.start_at_child_key = "aaa"; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant priority = 9999; + IndexedVariant old_snapshot(data, params); + + // Same as old_snapshot. + IndexedVariant expected_value(data, params); + EXPECT_EQ(filter.UpdatePriority(old_snapshot, priority), expected_value); +} + +TEST(RangedFilter, FiltersVariants) { + QueryParams params; + params.start_at_child_key = "aaa"; + RangedFilter filter(params); + EXPECT_TRUE(filter.FiltersVariants()); +} + +TEST(RangedFilter, StartAndEndPost) { + // Priority + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = std::make_pair( + "aaa", std::map{std::make_pair(".priority", "bbb")}); + std::pair expected_end_post = std::make_pair( + "ccc", std::map{std::make_pair(".priority", "ddd")}); + + EXPECT_EQ(start_post, expected_start_post); + EXPECT_EQ(end_post, expected_end_post); + } + + // Child + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + params.order_by_child = "zzz"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = std::make_pair( + "aaa", std::map{std::make_pair("zzz", "bbb")}); + std::pair expected_end_post = std::make_pair( + "ccc", std::map{std::make_pair("zzz", "ddd")}); + + EXPECT_EQ(start_post, expected_start_post) + << util::VariantToJson(start_post.first) << " | " + << util::VariantToJson(start_post.second); + EXPECT_EQ(end_post, expected_end_post) + << util::VariantToJson(end_post.first) << " | " + << util::VariantToJson(end_post.second); + } + + // Key + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = + std::make_pair("bbb", Variant::Null()); + std::pair expected_end_post = + std::make_pair("ddd", Variant::Null()); + + EXPECT_EQ(start_post, expected_start_post); + EXPECT_EQ(end_post, expected_end_post); + } + + // Value + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = + std::make_pair("aaa", "bbb"); + std::pair expected_end_post = + std::make_pair("ccc", "ddd"); + + EXPECT_EQ(start_post, expected_start_post); + EXPECT_EQ(end_post, expected_end_post); + } +} + +TEST(RangedFilter, MatchesByPriority) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.start_at_child_key = "ccc"; + params.start_at_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.end_at_child_key = "ccc"; + params.end_at_value = 300; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_TRUE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.start_at_child_key = "bbb"; + params.start_at_value = 200; + params.end_at_child_key = "ddd"; + params.end_at_value = 400; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.equal_to_child_key = "ccc"; + params.equal_to_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } +} + +TEST(RangedFilter, MatchesByChild) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.start_at_child_key = "ccc"; + params.start_at_value = 300; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.end_at_child_key = "ccc"; + params.end_at_value = 300; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.start_at_child_key = "bbb"; + params.start_at_value = 200; + params.end_at_child_key = "ddd"; + params.end_at_value = 400; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.equal_to_child_key = "ccc"; + params.equal_to_value = 300; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } +} + +TEST(RangedFilter, MatchesByKey) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "ccc"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("eee", 500))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.end_at_value = "ccc"; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "bbb"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.equal_to_value = "ccc"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } +} + +TEST(RangedFilter, MatchesByValue) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.start_at_child_key = "ccc"; + params.start_at_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("eee", 500))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.end_at_child_key = "ccc"; + params.end_at_value = 300; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.start_at_child_key = "bbb"; + params.start_at_value = 200; + params.end_at_child_key = "ddd"; + params.end_at_value = 400; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.equal_to_child_key = "ccc"; + params.equal_to_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/view_cache_test.cc b/database/tests/desktop/view/view_cache_test.cc new file mode 100644 index 0000000000..ec837a5c96 --- /dev/null +++ b/database/tests/desktop/view/view_cache_test.cc @@ -0,0 +1,133 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/view_cache.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(ViewCacheTest, Constructors) { + // Everything should be uninitialized. + ViewCache blank_cache; + // Local + EXPECT_EQ(blank_cache.local_snap().variant(), Variant::Null()); + EXPECT_FALSE(blank_cache.local_snap().fully_initialized()); + EXPECT_FALSE(blank_cache.local_snap().filtered()); + // Server + EXPECT_EQ(blank_cache.server_snap().variant(), Variant::Null()); + EXPECT_FALSE(blank_cache.server_snap().fully_initialized()); + EXPECT_FALSE(blank_cache.server_snap().filtered()); + + CacheNode local_cache(IndexedVariant("local_value"), true, false); + CacheNode server_cache(IndexedVariant("server_value"), false, true); + ViewCache populated_cache(local_cache, server_cache); + // Local + EXPECT_EQ(populated_cache.local_snap().variant(), "local_value"); + EXPECT_TRUE(populated_cache.local_snap().fully_initialized()); + EXPECT_FALSE(populated_cache.local_snap().filtered()); + // Server + EXPECT_EQ(populated_cache.server_snap().variant(), "server_value"); + EXPECT_FALSE(populated_cache.server_snap().fully_initialized()); + EXPECT_TRUE(populated_cache.server_snap().filtered()); +} + +TEST(ViewCacheTest, GetCompleteSnaps) { + // Everything should be uninitialized. + ViewCache blank_cache; + EXPECT_EQ(blank_cache.GetCompleteLocalSnap(), nullptr); + EXPECT_EQ(blank_cache.GetCompleteServerSnap(), nullptr); + + // Initialize the local and server cache. + CacheNode local_cache(IndexedVariant("local_value"), true, true); + CacheNode server_cache(IndexedVariant("server_value"), true, true); + ViewCache populated_cache(local_cache, server_cache); + EXPECT_EQ(populated_cache.GetCompleteLocalSnap(), + &populated_cache.local_snap().variant()); + EXPECT_EQ(populated_cache.GetCompleteServerSnap(), + &populated_cache.server_snap().variant()); +} + +TEST(ViewCacheTest, UpdateLocalSnap) { + // Start uninitialized and update the local cache. + ViewCache view_cache; + ViewCache local_update = + view_cache.UpdateLocalSnap(IndexedVariant("local_value"), true, true); + // Local + EXPECT_STREQ(local_update.local_snap().variant().string_value(), + "local_value"); + EXPECT_TRUE(local_update.local_snap().fully_initialized()); + EXPECT_TRUE(local_update.local_snap().filtered()); + // Server (should be unchanged). + EXPECT_TRUE(local_update.server_snap().variant().is_null()); + EXPECT_FALSE(local_update.server_snap().fully_initialized()); + EXPECT_FALSE(local_update.server_snap().filtered()); +} + +TEST(ViewCacheTest, UpdateServerSnap) { + // Start uninitialized and update the server cache. + ViewCache view_cache; + ViewCache server_update = + view_cache.UpdateServerSnap(IndexedVariant("server_value"), true, true); + // Local (should be unchanged). + EXPECT_TRUE(server_update.local_snap().variant().is_null()); + EXPECT_FALSE(server_update.local_snap().fully_initialized()); + EXPECT_FALSE(server_update.local_snap().filtered()); + // Server + EXPECT_STREQ(server_update.server_snap().variant().string_value(), + "server_value"); + EXPECT_TRUE(server_update.server_snap().fully_initialized()); + EXPECT_TRUE(server_update.server_snap().filtered()); +} + +TEST(ViewCacheTest, CacheNodeEquality) { + CacheNode cache_node(IndexedVariant("some_string"), true, true); + CacheNode same_cache_node(IndexedVariant("some_string"), true, true); + CacheNode different_variant(IndexedVariant("different_string"), true, true); + CacheNode different_fully_initialized(IndexedVariant("some_string"), false, + true); + CacheNode different_filtered(IndexedVariant("some_string"), true, false); + + EXPECT_EQ(cache_node, same_cache_node); + EXPECT_NE(cache_node, different_variant); + EXPECT_NE(cache_node, different_fully_initialized); + EXPECT_NE(cache_node, different_filtered); +} + +TEST(ViewCacheTest, ViewCacheEquality) { + CacheNode local_cache(IndexedVariant("local_value"), true, true); + CacheNode server_cache(IndexedVariant("server_value"), true, true); + ViewCache view_cache(local_cache, server_cache); + ViewCache same_view_cache(local_cache, server_cache); + + CacheNode different_local_cache_node(IndexedVariant("wrong_local_value"), + true, true); + CacheNode different_server_cache_node(IndexedVariant("server_value"), false, + true); + ViewCache different_local_cache(different_local_cache_node, server_cache); + ViewCache different_server_cache(local_cache, different_server_cache_node); + + EXPECT_EQ(view_cache, same_view_cache); + EXPECT_NE(view_cache, different_local_cache); + EXPECT_NE(view_cache, different_server_cache); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/view_processor_test.cc b/database/tests/desktop/view/view_processor_test.cc new file mode 100644 index 0000000000..296ca3c31e --- /dev/null +++ b/database/tests/desktop/view/view_processor_test.cc @@ -0,0 +1,727 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/view_processor.h" + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "database/src/desktop/core/operation.h" +#include "database/src/desktop/util_desktop.h" +#include "database/src/desktop/view/indexed_filter.h" + +// There are four types of operations we can apply: Overwrites, Merges, +// AckUserWrites, and ListenCompletes. Overwrites and merges can come from +// either the client or the server. AckUserWrites and ListenCompletes only come +// from the server. A test has been written for each combination of Operation +// type and operation source, and in the cases where there are significantly +// diverging code paths within a given conbination, multiple tests have been +// written to test each code path. + +using ::testing::Eq; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(ViewProcessor, Constructor) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // No tests, just making sure the indexed filter doesn't leak after + // destruction. +} + +// Apply an Overwrite operation that was initiated by the user, using an empty +// path. +TEST(ViewProcessor, ApplyOperationUserOverwrite_WithEmptyPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a user-initiated overwrite with an empty path to change a value. + Operation operation = + Operation::Overwrite(OperationSource::kUser, Path(), Variant("apples")); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache(Variant("apples"), true, false); + ViewCache expected_view_cache(expected_local_cache, initial_server_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(Variant("apples"))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the user, using a +// .priority path. +TEST(ViewProcessor, ApplyOperationUserOverwrite_WithPriorityPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a user-initiated overwrite with an empty path to change a value. + Operation operation = Operation::Overwrite(OperationSource::kUser, + Path(".priority"), Variant(100)); + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache(CombineValueAndPriority("local_values", 100), + true, false); + ViewCache expected_view_cache(expected_local_cache, initial_server_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(CombineValueAndPriority("local_values", 100))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the user, regular +// non-empty path. +TEST(ViewProcessor, ApplyOperationUserOverwrite_WithRegularPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a user-initiated overwrite with a non-empty path to change a value. + Operation operation = Operation::Overwrite( + OperationSource::kUser, Path("aaa/bbb"), Variant("apples")); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path("aaa/bbb")); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }), + true, false); + ViewCache expected_view_cache(expected_local_cache, initial_server_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildAddedChange("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the server, using an empty +// path. +TEST(ViewProcessor, ApplyOperationServerOverwrite_WithEmptyPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a server-initiated overwrite with an empty path to change a value. + Operation operation = + Operation::Overwrite(OperationSource::kServer, Path(), Variant("apples")); + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both the local and server caches have been set. + CacheNode expected_cache(Variant("apples"), true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(Variant("apples"))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the server, using a +// regular path. +TEST(ViewProcessor, ApplyOperationServerOverwrite_RegularPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + Operation operation = + Operation::Overwrite(OperationSource::kServer, Path("aaa"), + Variant(std::map{ + std::make_pair("bbb", "apples"), + })); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both caches are expected to be the same. + CacheNode expected_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }), + true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect one ChildAdded event and one Value event. + std::vector expected_changes{ + ChildAddedChange("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }))), + }; + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the server, using a path +// that is deeper than a direct child of the location. +TEST(ViewProcessor, ApplyOperationServerOverwrite_DistantDescendantChange) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_cache(Variant(std::map{std::make_pair( + "aaa", + std::map{std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", 1000), + })})}), + true, false); + ViewCache old_view_cache(initial_cache, initial_cache); + + // Make sure the data being updated is deeply nested in the variant. + Operation operation = Operation::Overwrite( + OperationSource::kServer, Path("aaa/bbb/ccc"), Variant(-9999)); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both caches are expected to be the same. + CacheNode expected_cache(Variant(std::map{std::make_pair( + "aaa", + std::map{std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", -9999), + })})}), + true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildChangedChange("aaa", + IndexedVariant(Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", -9999), + })})), + IndexedVariant(Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 1000), + })}))), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", -9999), + })})}))), + }; + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply a Merge operation that was initiated by the user. +TEST(ViewProcessor, ApplyOperationUserMerge) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "zzz"), + }), + }), + true, false); + CacheNode initial_server_cache(Variant("aaa"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + // The merge operation should consist of multiple changes in different + // locations. + CompoundWrite write = + CompoundWrite() + .AddWrite(Path("aaa/bbb/ccc"), Variant("apples")) + .AddWrite(Path("aaa/ddd"), Variant("bananas")) + .AddWrite(Path("aaa/eee/fff"), Variant("vegetables")); + Operation operation = Operation::Merge(OperationSource::kUser, Path(), write); + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache( + Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", "apples"), + }), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + true, false); + CacheNode expected_server_cache(Variant("aaa"), true, false); + ViewCache expected_view_cache(expected_local_cache, expected_server_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildChangedChange( + "aaa", + Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", "apples"), + }), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + Variant(std::map{ + std::make_pair("bbb", "zzz"), + })), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", "apples"), + }), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationServerMerge) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "zzz"), + }), + }), + true, false); + CacheNode initial_server_cache(Variant("aaa"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // The merge operation should consist of multiple changes in different + // locations. + CompoundWrite write = + CompoundWrite() + .AddWrite(Path("bbb/ccc"), Variant("apples")) + .AddWrite(Path("bbb/ddd"), Variant("bananas")) + .AddWrite(Path("bbb/eee/fff"), Variant("vegetables")); + Operation operation = + Operation::Merge(OperationSource::kServer, Path("aaa"), write); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path("aaa")); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both caches are expected to be the same. + CacheNode expected_cache( + Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", "apples"), + std::make_pair("ddd", "bananas"), + std::make_pair( + "eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + }), + true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildChangedChange( + "aaa", + Variant(std::map{ + std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", "apples"), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + Variant(std::map{ + std::make_pair("bbb", "zzz"), + })), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", "apples"), + std::make_pair("ddd", "bananas"), + std::make_pair( + "eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationAck_HasShadowingWrite) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create an Ack with a shadowing write. + // These values don't matter for this test because the shadowing write will + // short circuit everything. + Tree affected_tree; + Operation operation = + Operation::AckUserWrite(Path("aaa"), affected_tree, kAckConfirm); + + // Set up shadowing write. + WriteTree writes_cache; + writes_cache.AddOverwrite(Path("aaa"), Variant("overwrite"), 100, + kOverwriteVisible); + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Expect no changes in the view cache. + ViewCache expected_view_cache = old_view_cache; + + // Expect no Changes as a result of this. + std::vector expected_changes; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationAck_IsOverwrite) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "new_value"), + }), + }), + true, false); + CacheNode initial_server_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "new_value"), + }), + }), + true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + Tree affected_tree; + affected_tree.set_value(true); + affected_tree.SetValueAt(Path("aaa/bbb"), true); + Operation operation = + Operation::AckUserWrite(Path(), affected_tree, kAckConfirm); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + writes_cache.AddOverwrite(Path("aaa/bbb"), "new_value", 1234, + kOverwriteVisible); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Expect no changes in the view cache. + ViewCache expected_view_cache(initial_local_cache, initial_server_cache); + + // Expect no Changes as a result of this. + std::vector expected_changes; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationAckRevert) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "new_value"), + }), + }), + true, false); + CacheNode initial_server_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "old_value"), + }), + }), + true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Mark the value we're going to be reverting. + Tree affected_tree; + affected_tree.set_value(true); + affected_tree.SetValueAt(Path("aaa/bbb"), true); + Operation operation = + Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + // Hold the old value in the writes cache. + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + writes_cache.AddOverwrite(Path("aaa/bbb"), "old_value", 1234, + kOverwriteVisible); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Expect that the local cache gets reverted to the old value. + ViewCache expected_view_cache(initial_server_cache, initial_server_cache); + + // Expect a ChildChanged and Value Changes, setting things back to the old + // value. + std::vector expected_changes{ + ChildChangedChange("aaa", + Variant(std::map{ + std::make_pair("bbb", "old_value"), + }), + Variant(std::map{ + std::make_pair("bbb", "new_value"), + })), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "old_value"), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationListenComplete) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a server-initiated listen complete with an empty path to change a + // value. + Operation operation = + Operation::ListenComplete(OperationSource::kServer, Path()); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // The local cache should now reflect the server cache. + ViewCache expected_view_cache(initial_server_cache, initial_server_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(Variant("server_values"))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/view_test.cc b/database/tests/desktop/view/view_test.cc new file mode 100644 index 0000000000..fab8dc4eb3 --- /dev/null +++ b/database/tests/desktop/view/view_test.cc @@ -0,0 +1,464 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/view.h" + +#include "app/memory/unique_ptr.h" +#include "app/src/variant_util.h" +#include "database/src/desktop/core/event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/core/write_tree.h" +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/desktop/view/view_cache.h" +#include "database/src/include/firebase/database/common.h" +#include "database/tests/desktop/test/matchers.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::Not; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(View, Constructor) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode local_cache(IndexedVariant(Variant(), query_spec.params), true, + true); + CacheNode server_cache(IndexedVariant(Variant(), query_spec.params), true, + false); + ViewCache initial_view_cache(local_cache, server_cache); + + View view(query_spec, initial_view_cache); + + EXPECT_EQ(view.query_spec(), query_spec); + EXPECT_EQ(view.view_cache(), initial_view_cache); +} + +TEST(View, MoveConstructor) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), query_spec.params), true, + false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + View new_view(std::move(old_view)); + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +TEST(View, MoveAssignment) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), params), true, false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + // When we move the old_view into the new_view, make sure any existing + // registrations are properly cleaned up and not leaked. + ValueEventRegistration* registration_to_be_deleted = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + View new_view((QuerySpec()), ViewCache(CacheNode(), CacheNode())); + new_view.AddEventRegistration( + UniquePtr(registration_to_be_deleted)); + + new_view = std::move(old_view); + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +// For Views, copies are actually moves, so this test is identical to the +// MoveConstructor test. +TEST(View, CopyConstructor) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), params), true, false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + View new_view(old_view); + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +// For Views, copies are actually moves, so this test is identical to the +// MoveAssignment test. +TEST(View, CopyAssignment) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), params), true, false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + // When we move the old_view into the new_view, make sure any existing + // registrations are properly cleaned up and not leaked. + ValueEventRegistration* registration_to_be_deleted = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + View new_view((QuerySpec()), ViewCache(CacheNode(), CacheNode())); + new_view.AddEventRegistration( + UniquePtr(registration_to_be_deleted)); + + new_view = old_view; + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +TEST(View, GetCompleteServerCache_Empty) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(), query_spec.params), true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + EXPECT_EQ(view.GetCompleteServerCache(Path("test/path")), nullptr); +} + +TEST(View, GetCompleteServerCache_NonEmpty) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(std::map{ + std::make_pair("foo", "bar"), + std::make_pair("baz", "quux"), + }), + query_spec.params), + true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + EXPECT_EQ(*view.GetCompleteServerCache(Path("foo")), "bar"); +} + +TEST(View, IsNotEmpty) { + QuerySpec query_spec; + ViewCache initial_view_cache; + View view(query_spec, initial_view_cache); + + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration)); + + EXPECT_FALSE(view.IsEmpty()); +} + +TEST(View, IsEmpty) { + QuerySpec query_spec; + ViewCache initial_view_cache; + View view(query_spec, initial_view_cache); + + EXPECT_TRUE(view.IsEmpty()); +} + +TEST(View, AddEventRegistration) { + QuerySpec query_spec; + ViewCache initial_view_cache; + View view(query_spec, initial_view_cache); + + ValueEventRegistration* registration1 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + ValueEventRegistration* registration2 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + ValueEventRegistration* registration3 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + ValueEventRegistration* registration4 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration1)); + view.AddEventRegistration(UniquePtr(registration2)); + view.AddEventRegistration(UniquePtr(registration3)); + view.AddEventRegistration(UniquePtr(registration4)); + + std::vector expected_registrations{ + registration1, + registration2, + registration3, + registration4, + }; + + EXPECT_THAT(view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), expected_registrations)); +} + +class DummyValueListener : public ValueListener { + public: + ~DummyValueListener() override {} + void OnValueChanged(const DataSnapshot& snapshot) override {} + void OnCancelled(const Error& error, const char* error_message) override {} +}; + +TEST(View, RemoveEventRegistration_RemoveOne) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(), query_spec.params), true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + DummyValueListener listener1; + DummyValueListener listener2; + DummyValueListener listener3; + DummyValueListener listener4; + + ValueEventRegistration* registration1 = + new ValueEventRegistration(nullptr, &listener1, QuerySpec()); + ValueEventRegistration* registration2 = + new ValueEventRegistration(nullptr, &listener2, QuerySpec()); + ValueEventRegistration* registration3 = + new ValueEventRegistration(nullptr, &listener3, QuerySpec()); + ValueEventRegistration* registration4 = + new ValueEventRegistration(nullptr, &listener4, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration1)); + view.AddEventRegistration(UniquePtr(registration2)); + view.AddEventRegistration(UniquePtr(registration3)); + view.AddEventRegistration(UniquePtr(registration4)); + + std::vector expected_events{}; + std::vector expected_registrations{ + registration1, + registration2, + registration4, + }; + + EXPECT_THAT( + view.RemoveEventRegistration(static_cast(&listener3), kErrorNone), + Pointwise(Eq(), expected_events)); +} + +TEST(View, RemoveEventRegistration_RemoveAll) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(), query_spec.params), true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + DummyValueListener listener1; + DummyValueListener listener2; + DummyValueListener listener3; + DummyValueListener listener4; + + ValueEventRegistration* registration1 = + new ValueEventRegistration(nullptr, &listener1, QuerySpec()); + ValueEventRegistration* registration2 = + new ValueEventRegistration(nullptr, &listener2, QuerySpec()); + ValueEventRegistration* registration3 = + new ValueEventRegistration(nullptr, &listener3, QuerySpec()); + ValueEventRegistration* registration4 = + new ValueEventRegistration(nullptr, &listener4, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration1)); + view.AddEventRegistration(UniquePtr(registration2)); + view.AddEventRegistration(UniquePtr(registration3)); + view.AddEventRegistration(UniquePtr(registration4)); + + std::vector results = + view.RemoveEventRegistration(nullptr, kErrorDisconnected); + + EXPECT_EQ(results.size(), 4); + + EXPECT_EQ(results[0].type, kEventTypeError); + EXPECT_EQ(results[0].event_registration, registration1); + EXPECT_EQ(results[0].error, kErrorDisconnected); + EXPECT_EQ(results[0].path, Path("test/path")); + EXPECT_EQ(results[0].event_registration_ownership_ptr.get(), registration1); + + EXPECT_EQ(results[1].type, kEventTypeError); + EXPECT_EQ(results[1].event_registration, registration2); + EXPECT_EQ(results[1].error, kErrorDisconnected); + EXPECT_EQ(results[1].path, Path("test/path")); + EXPECT_EQ(results[1].event_registration_ownership_ptr.get(), registration2); + + EXPECT_EQ(results[2].type, kEventTypeError); + EXPECT_EQ(results[2].event_registration, registration3); + EXPECT_EQ(results[2].error, kErrorDisconnected); + EXPECT_EQ(results[2].path, Path("test/path")); + EXPECT_EQ(results[2].event_registration_ownership_ptr.get(), registration3); + + EXPECT_EQ(results[3].type, kEventTypeError); + EXPECT_EQ(results[3].event_registration, registration4); + EXPECT_EQ(results[3].error, kErrorDisconnected); + EXPECT_EQ(results[3].path, Path("test/path")); + EXPECT_EQ(results[3].event_registration_ownership_ptr.get(), registration4); +} + +// View::ApplyOperation tests omitted. It just calls through to the functions +// ViewProcessor::ApplyOperation and GenerateEventsForChanges, and it is +// difficult to mock the interaction. Those functions are themselves tested in +// view_processor_test.cc and event_generator_test.cc respectively. + +TEST(ViewDeathTest, ApplyOperation_MustHaveLocalCache) { + QuerySpec query_spec; + CacheNode local_cache(IndexedVariant(Variant()), true, false); + CacheNode server_cache(IndexedVariant(Variant()), false, false); + ViewCache initial_view_cache(local_cache, server_cache); + View view(query_spec, initial_view_cache); + + Operation operation(Operation::kTypeMerge, + OperationSource(Optional()), Path(), + Variant(), CompoundWrite(), Tree(), kAckConfirm); + WriteTree write_tree; + WriteTreeRef writes_cache(Path(), &write_tree); + Variant complete_server_cache; + std::vector changes; + + EXPECT_DEATH(view.ApplyOperation(operation, writes_cache, + &complete_server_cache, &changes), + DEATHTEST_SIGABRT); +} + +TEST(ViewDeathTest, ApplyOperation_MustHaveServerCache) { + QuerySpec query_spec; + CacheNode local_cache(IndexedVariant(Variant()), false, false); + CacheNode server_cache(IndexedVariant(Variant()), true, false); + ViewCache initial_view_cache(local_cache, server_cache); + View view(query_spec, initial_view_cache); + + Operation operation(Operation::kTypeMerge, + OperationSource(Optional()), Path(), + Variant(), CompoundWrite(), Tree(), kAckConfirm); + WriteTree write_tree; + WriteTreeRef writes_cache(Path(), &write_tree); + Variant complete_server_cache; + std::vector changes; + + EXPECT_DEATH(view.ApplyOperation(operation, writes_cache, + &complete_server_cache, &changes), + DEATHTEST_SIGABRT); +} + +TEST(View, GetInitialEvents) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + CacheNode cache(IndexedVariant(Variant(std::map{ + std::make_pair("foo", "bar"), + std::make_pair("baz", "quux"), + }), + query_spec.params), + true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + ValueEventRegistration registration(nullptr, nullptr, QuerySpec()); + + std::vector results = view.GetInitialEvents(®istration); + std::vector expected_results{ + Event(kEventTypeValue, ®istration, + DataSnapshotInternal(nullptr, + Variant(std::map{ + std::make_pair("foo", "bar"), + std::make_pair("baz", "quux"), + }), + query_spec), + ""), + }; + + EXPECT_THAT(results, Pointwise(Eq(), expected_results)); +} + +TEST(View, GetEventCache) { + CacheNode local_cache(IndexedVariant(Variant("Apples")), false, false); + CacheNode server_cache(IndexedVariant(Variant("Bananas")), true, false); + ViewCache initial_view_cache(local_cache, server_cache); + View view(QuerySpec(), initial_view_cache); + + EXPECT_EQ(view.GetLocalCache(), "Apples"); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/firestore/generate_android_test.py b/firestore/generate_android_test.py new file mode 100755 index 0000000000..83c039d04d --- /dev/null +++ b/firestore/generate_android_test.py @@ -0,0 +1,88 @@ +#!/usr/grte/v4/bin/python2.7 +"""Generate JUnit4 tests from gtest files. + +This script reads a template and fills in test-specific information such as .so +library name and Java class name. This script also goes over the gtest files and +finds all test methods of the pattern TEST_F(..., ...) and converts each into a +@Test-annotated test method. +""" + +# We will be open-source this. So please do not introduce google3 dependency +# unless absolutely necessary. + +import argparse +import re + +GTEST_METHOD_RE = (r'TEST_F[(]\s*(?P[A-Za-z]+)\s*,\s*' + r'(?P[A-Za-z]+)\s*[)]') + +JAVA_TEST_METHOD = r""" + @Test + public void {test_class}{test_method}() {{ + run("{test_class}.{test_method}"); + }} +""" + + +def generate_fragment(gtests): + """Generate @Test-annotated test method code from the provided gtest files.""" + fragments = [] + gtest_method_pattern = re.compile(GTEST_METHOD_RE) + for gtest in gtests: + with open(gtest, 'r') as gtest_file: + gtest_code = gtest_file.read() + for matched in re.finditer(gtest_method_pattern, gtest_code): + fragments.append( + JAVA_TEST_METHOD.format( + test_class=matched.group('test_class'), + test_method=matched.group('test_method'))) + return ''.join(fragments) + + +def generate_file(template, out, **kwargs): + """Generate a Java file from the provided template and parameters.""" + with open(template, 'r') as template_file: + template_string = template_file.read() + java_code = template_string.format(**kwargs) + with open(out, 'w') as out_file: + out_file.write(java_code) + + +def main(): + parser = argparse.ArgumentParser( + description='Generates JUnit4 tests from gtest files.') + parser.add_argument( + '--template', + help='the filename of the template to use in the generation', + required=True) + parser.add_argument( + '--java_package', + help='which package test Java class belongs to', + required=True) + parser.add_argument( + '--java_class', + help='specifies the name of the class to generate', + required=True) + parser.add_argument( + '--so_lib', + help=('specifies the name of the native library without prefix lib and ' + 'suffix .so. You must compile the C++ test code together with the ' + 'firestore_android_test_main.cc as a shared library, say libfoo.so ' + 'and pass the name foo here.'), + required=True) + parser.add_argument('--out', help='the output file path', required=True) + parser.add_argument('srcs', nargs='+', help='the input gtest file paths') + args = parser.parse_args() + + fragment = generate_fragment(args.srcs) + generate_file( + args.template, + args.out, + package_name=args.java_package, + java_class_name=args.java_class, + so_lib_name=args.so_lib, + tests=fragment) + + +if __name__ == '__main__': + main() diff --git a/firestore/src/tests/android/field_path_portable_test.cc b/firestore/src/tests/android/field_path_portable_test.cc new file mode 100644 index 0000000000..925bbd7eb0 --- /dev/null +++ b/firestore/src/tests/android/field_path_portable_test.cc @@ -0,0 +1,140 @@ +#include "firestore/src/android/field_path_portable.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +// The test cases are copied from +// Firestore/core/test/firebase/firestore/model/field_path_test.cc + +TEST(FieldPathPortableTest, Indexing) { + const FieldPathPortable path({"rooms", "Eros", "messages"}); + + EXPECT_EQ(path[0], "rooms"); + EXPECT_EQ(path[1], "Eros"); + EXPECT_EQ(path[2], "messages"); +} + +TEST(FieldPathPortableTest, Comparison) { + const FieldPathPortable abc({"a", "b", "c"}); + const FieldPathPortable abc2({"a", "b", "c"}); + const FieldPathPortable xyz({"x", "y", "z"}); + EXPECT_EQ(abc, abc2); + EXPECT_NE(abc, xyz); + + const FieldPathPortable empty({}); + const FieldPathPortable a({"a"}); + const FieldPathPortable b({"b"}); + const FieldPathPortable ab({"a", "b"}); + + EXPECT_TRUE(empty < a); + EXPECT_TRUE(a < b); + EXPECT_TRUE(a < ab); + + EXPECT_TRUE(a > empty); + EXPECT_TRUE(b > a); + EXPECT_TRUE(ab > a); +} + +TEST(FieldPathPortableTest, CanonicalStringOfSubstring) { + EXPECT_EQ(FieldPathPortable({"foo", "bar", "baz"}).CanonicalString(), + "foo.bar.baz"); + EXPECT_EQ(FieldPathPortable({"foo", "bar"}).CanonicalString(), "foo.bar"); + EXPECT_EQ(FieldPathPortable({"foo"}).CanonicalString(), "foo"); + EXPECT_EQ(FieldPathPortable({}).CanonicalString(), ""); +} + +TEST(FieldPath, CanonicalStringEscaping) { + // Should be escaped + EXPECT_EQ(FieldPathPortable({"1"}).CanonicalString(), "`1`"); + EXPECT_EQ(FieldPathPortable({"1ab"}).CanonicalString(), "`1ab`"); + EXPECT_EQ(FieldPathPortable({"ab!"}).CanonicalString(), "`ab!`"); + EXPECT_EQ(FieldPathPortable({"/ab"}).CanonicalString(), "`/ab`"); + EXPECT_EQ(FieldPathPortable({"a#b"}).CanonicalString(), "`a#b`"); + EXPECT_EQ(FieldPathPortable({"foo", "", "bar"}).CanonicalString(), + "foo.``.bar"); + + // Should not be escaped + EXPECT_EQ(FieldPathPortable({"_ab"}).CanonicalString(), "_ab"); + EXPECT_EQ(FieldPathPortable({"a1"}).CanonicalString(), "a1"); + EXPECT_EQ(FieldPathPortable({"a_"}).CanonicalString(), "a_"); +} + +TEST(FieldPathPortableTest, Parsing) { + EXPECT_EQ(FieldPathPortable::FromServerFormat("foo"), + FieldPathPortable({"foo"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat("foo.bar"), + FieldPathPortable({"foo", "bar"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat("foo.bar.baz"), + FieldPathPortable({"foo", "bar", "baz"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(`.foo\\`)"), + FieldPathPortable({".foo\\"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(`.foo\\`.`.foo`)"), + FieldPathPortable({".foo\\", ".foo"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(foo.`\``.bar)"), + FieldPathPortable({"foo", "`", "bar"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(foo\.bar)"), + FieldPathPortable({"foo.bar"})); +} + +// This is a special case in C++: std::string may contain embedded nulls. To +// fully mimic behavior of Objective-C code, parsing must terminate upon +// encountering the first null terminator in the string. +TEST(FieldPathPortableTest, ParseEmbeddedNull) { + std::string str{"foo"}; + str += '\0'; + str += ".bar"; + + const auto path = FieldPathPortable::FromServerFormat(str); + EXPECT_EQ(path.size(), 1u); + EXPECT_EQ(path.CanonicalString(), "foo"); +} + +TEST(FieldPathPortableDeathTest, ParseFailures) { + EXPECT_DEATH(FieldPathPortable::FromServerFormat(""), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("."), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(".."), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo."), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(".bar"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo..bar"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(R"(foo\)"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(R"(foo.\)"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo`"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo```"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("`foo"), ""); +} + +TEST(FieldPathPortableTest, FromDotSeparatedString) { + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("a"), + FieldPathPortable({"a"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("foo"), + FieldPathPortable({"foo"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("a.b"), + FieldPathPortable({"a", "b"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("foo.bar"), + FieldPathPortable({"foo", "bar"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("foo.bar.baz"), + FieldPathPortable({"foo", "bar", "baz"})); +} + +TEST(FieldPathPortableDeathTest, FromDotSeparatedStringParseFailures) { + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString(""), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString("."), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString(".foo"), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString("foo."), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString("foo..bar"), ""); +} + +TEST(FieldPathPortableTest, KeyFieldPath) { + const auto& key_field_path = FieldPathPortable::KeyFieldPath(); + EXPECT_TRUE(key_field_path.IsKeyFieldPath()); + EXPECT_EQ(key_field_path, FieldPathPortable{key_field_path}); + EXPECT_EQ(key_field_path.CanonicalString(), "__name__"); + EXPECT_EQ(key_field_path, FieldPathPortable::FromServerFormat("__name__")); + EXPECT_NE(key_field_path, FieldPathPortable::FromServerFormat( + key_field_path.CanonicalString().substr(1))); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/firebase_firestore_settings_android_test.cc b/firestore/src/tests/android/firebase_firestore_settings_android_test.cc new file mode 100644 index 0000000000..22f43f159c --- /dev/null +++ b/firestore/src/tests/android/firebase_firestore_settings_android_test.cc @@ -0,0 +1,52 @@ +#include "firestore/src/android/firebase_firestore_settings_android.h" + +#include + +#include "firestore/src/include/firebase/firestore/settings.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, ConverterBoolsAllTrue) { + JNIEnv* env = app()->GetJNIEnv(); + + Settings settings; + settings.set_host("foo"); + settings.set_ssl_enabled(true); + settings.set_persistence_enabled(true); + jobject java_settings = + FirebaseFirestoreSettingsInternal::SettingToJavaSetting(env, settings); + const Settings result = + FirebaseFirestoreSettingsInternal::JavaSettingToSetting(env, + java_settings); + EXPECT_EQ("foo", result.host()); + EXPECT_TRUE(result.is_ssl_enabled()); + EXPECT_TRUE(result.is_persistence_enabled()); + + env->DeleteLocalRef(java_settings); +} + +TEST_F(FirestoreIntegrationTest, ConverterBoolsAllFalse) { + JNIEnv* env = app()->GetJNIEnv(); + + Settings settings; + settings.set_host("bar"); + settings.set_ssl_enabled(false); + settings.set_persistence_enabled(false); + jobject java_settings = + FirebaseFirestoreSettingsInternal::SettingToJavaSetting(env, settings); + const Settings result = + FirebaseFirestoreSettingsInternal::JavaSettingToSetting(env, + java_settings); + EXPECT_EQ("bar", result.host()); + EXPECT_FALSE(result.is_ssl_enabled()); + EXPECT_FALSE(result.is_persistence_enabled()); + + env->DeleteLocalRef(java_settings); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/geo_point_android_test.cc b/firestore/src/tests/android/geo_point_android_test.cc new file mode 100644 index 0000000000..8d09aee483 --- /dev/null +++ b/firestore/src/tests/android/geo_point_android_test.cc @@ -0,0 +1,24 @@ +#include "firestore/src/android/geo_point_android.h" + +#include + +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/geo_point.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, Converter) { + JNIEnv* env = app()->GetJNIEnv(); + + const GeoPoint point{12.0, 34.0}; + jobject java_point = GeoPointInternal::GeoPointToJavaGeoPoint(env, point); + EXPECT_EQ(point, GeoPointInternal::JavaGeoPointToGeoPoint(env, java_point)); + + env->DeleteLocalRef(java_point); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/snapshot_metadata_android_test.cc b/firestore/src/tests/android/snapshot_metadata_android_test.cc new file mode 100644 index 0000000000..3781697f78 --- /dev/null +++ b/firestore/src/tests/android/snapshot_metadata_android_test.cc @@ -0,0 +1,42 @@ +#include "firestore/src/android/snapshot_metadata_android.h" + +#include + +#include "firestore/src/include/firebase/firestore/snapshot_metadata.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, ConvertHasPendingWrites) { + JNIEnv* env = app()->GetJNIEnv(); + + const SnapshotMetadata has_pending_writes{/*has_pending_writes=*/true, + /*is_from_cache=*/false}; + // The converter will delete local reference for us. + const SnapshotMetadata result = + SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata( + env, SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata( + env, has_pending_writes)); + EXPECT_TRUE(result.has_pending_writes()); + EXPECT_FALSE(result.is_from_cache()); +} + +TEST_F(FirestoreIntegrationTest, ConvertIsFromCache) { + JNIEnv* env = app()->GetJNIEnv(); + + const SnapshotMetadata is_from_cache{/*has_pending_writes=*/false, + /*is_from_cache=*/true}; + // The converter will delete local reference for us. + const SnapshotMetadata result = + SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata( + env, SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata( + env, is_from_cache)); + EXPECT_FALSE(result.has_pending_writes()); + EXPECT_TRUE(result.is_from_cache()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/timestamp_android_test.cc b/firestore/src/tests/android/timestamp_android_test.cc new file mode 100644 index 0000000000..a30c5373c9 --- /dev/null +++ b/firestore/src/tests/android/timestamp_android_test.cc @@ -0,0 +1,26 @@ +#include "firestore/src/android/timestamp_android.h" + +#include + +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/timestamp.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, Converter) { + JNIEnv* env = app()->GetJNIEnv(); + + const Timestamp timestamp{1234, 5678}; + jobject java_timestamp = + TimestampInternal::TimestampToJavaTimestamp(env, timestamp); + EXPECT_EQ(timestamp, + TimestampInternal::JavaTimestampToTimestamp(env, java_timestamp)); + + env->DeleteLocalRef(java_timestamp); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/array_transform_test.cc b/firestore/src/tests/array_transform_test.cc new file mode 100644 index 0000000000..38ce96cb35 --- /dev/null +++ b/firestore/src/tests/array_transform_test.cc @@ -0,0 +1,230 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRArrayTransformTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ArrayTransformsTest.java +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ArrayTransformServerApplicationTest.java + +namespace firebase { +namespace firestore { + +class ArrayTransformTest : public FirestoreIntegrationTest { + protected: + void SetUp() override { + document_ = Document(); + registration_ = accumulator_.listener()->AttachTo( + &document_, MetadataChanges::kInclude); + + // Wait for initial null snapshot to avoid potential races. + DocumentSnapshot snapshot = accumulator_.Await(); + EXPECT_FALSE(snapshot.exists()); + } + + void TearDown() override { registration_.Remove(); } + + void WriteInitialData(const MapFieldValue& data) { + Await(document_.Set(data)); + ExpectLocalAndRemoteEvent(data); + } + + void ExpectLocalAndRemoteEvent(const MapFieldValue& data) { + EXPECT_THAT(accumulator_.AwaitLocalEvent().GetData(), + testing::ContainerEq(data)); + EXPECT_THAT(accumulator_.AwaitRemoteEvent().GetData(), + testing::ContainerEq(data)); + } + + DocumentReference document_; + EventAccumulator accumulator_; + ListenerRegistration registration_; +}; + +class ArrayTransformServerApplicationTest : public FirestoreIntegrationTest { + protected: + void SetUp() override { document_ = Document(); } + + DocumentReference document_; +}; + +TEST_F(ArrayTransformTest, CreateDocumentWithArrayUnion) { + Await(document_.Set(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(2)})}}); +} + +TEST_F(ArrayTransformTest, AppendToArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Update(MapFieldValue{ + {"array", + FieldValue::ArrayUnion({FieldValue::Integer(2), FieldValue::Integer(1), + FieldValue::Integer(4)})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(2), FieldValue::Integer(4)})}}); +} + +TEST_F(ArrayTransformTest, AppendToArrayViaMergeSet) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Set(MapFieldValue{{"array", FieldValue::ArrayUnion( + {FieldValue::Integer(2), + FieldValue::Integer(1), + FieldValue::Integer(4)})}}, + SetOptions::Merge())); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(2), FieldValue::Integer(4)})}}); +} + +TEST_F(ArrayTransformTest, AppendObjectToArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::String("hi")}})})}}); + Await(document_.Update(MapFieldValue{ + {"array", + FieldValue::ArrayUnion( + {{FieldValue::Map({{"a", FieldValue::String("hi")}})}, + {FieldValue::Map({{"a", FieldValue::String("bye")}})}})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", FieldValue::Array( + {{FieldValue::Map({{"a", FieldValue::String("hi")}})}, + {FieldValue::Map({{"a", FieldValue::String("bye")}})}})}}); +} + +TEST_F(ArrayTransformTest, RemoveFromArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayRemove( + {FieldValue::Integer(1), FieldValue::Integer(4)})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(3), FieldValue::Integer(3)})}}); +} + +TEST_F(ArrayTransformTest, RemoveFromArrayViaMergeSet) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Set(MapFieldValue{{"array", FieldValue::ArrayRemove( + {FieldValue::Integer(1), + FieldValue::Integer(4)})}}, + SetOptions::Merge())); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(3), FieldValue::Integer(3)})}}); +} + +TEST_F(ArrayTransformTest, RemoveObjectFromArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::String("hi")}}), + FieldValue::Map({{"a", FieldValue::String("bye")}})})}}); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayRemove( + {{FieldValue::Map({{"a", FieldValue::String("hi")}})}})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", FieldValue::Array( + {{FieldValue::Map({{"a", FieldValue::String("bye")}})}})}}); +} + +TEST_F(ArrayTransformServerApplicationTest, SetWithNoCachedBaseDoc) { + Await(document_.Set(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(1), + FieldValue::Integer(2)})}})); +} + +TEST_F(ArrayTransformServerApplicationTest, UpdateWithNoCachedBaseDoc) { + // Write an initial document in an isolated Firestore instance so it's not + // stored in our cache. + Await(CachedFirestore("isolated") + ->Document(document_.path()) + .Set(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42)})}})); + + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + + // Nothing should be cached since it was an update and we had no base doc. + Future future = document_.Get(Source::kCache); + Await(future); + EXPECT_EQ(Error::kErrorUnavailable, future.error()); +} + +TEST_F(ArrayTransformServerApplicationTest, MergeSetWithNoCachedBaseDoc) { + // Write an initial document in an isolated Firestore instance so it's not + // stored in our cache. + Await(CachedFirestore("isolated") + ->Document(document_.path()) + .Set(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42)})}})); + + Await(document_.Set(MapFieldValue{{"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), + FieldValue::Integer(2)})}}, + SetOptions::Merge())); + // Document will be cached but we'll be missing 42. + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(1), + FieldValue::Integer(2)})}})); +} + +TEST_F(ArrayTransformServerApplicationTest, + UpdateWithCachedBaseDocUsingArrayUnion) { + Await(document_.Set( + MapFieldValue{{"array", FieldValue::Array({FieldValue::Integer(42)})}})); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42), + FieldValue::Integer(1), + FieldValue::Integer(2)})}})); +} + +TEST_F(ArrayTransformServerApplicationTest, + UpdateWithCachedBaseDocUsingArrayRemove) { + Await(document_.Set(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(42), FieldValue::Integer(1L), + FieldValue::Integer(2L)})}})); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayRemove( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42)})}})); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/cleanup_test.cc b/firestore/src/tests/cleanup_test.cc new file mode 100644 index 0000000000..f23ee29be7 --- /dev/null +++ b/firestore/src/tests/cleanup_test.cc @@ -0,0 +1,420 @@ +#include "app/src/include/firebase/internal/common.h" +#include "firestore/src/common/futures.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" + +namespace firebase { +namespace firestore { +namespace { + +void ExpectAllMethodsAreNoOps(Query* ptr); + +// Checks that methods accessing the associated Firestore instance don't crash +// and return null. +template +void ExpectNullFirestore(T* ptr) { + EXPECT_EQ(ptr->firestore(), nullptr); + // Make sure to check both const and non-const overloads. + EXPECT_EQ(static_cast(ptr)->firestore(), nullptr); +} + +// Checks that the given object can be copied from, and the resulting copy can +// be moved. +template +void ExpectCopyableAndMoveable(T* ptr) { + EXPECT_NO_THROW({ + // Copy constructor + T copy = *ptr; + // Move constructor + T moved = std::move(copy); + + // Copy assignment operator + copy = *ptr; + // Move assignment operator + moved = std::move(copy); + }); +} + +// Checks that `operator==` and `operator!=` work correctly by comparing to +// a default-constructed instance. +template +void ExpectEqualityToWork(T* ptr) { + EXPECT_TRUE(*ptr == T()); + EXPECT_FALSE(*ptr != T()); +} + +// `ExpectAllMethodsAreNoOps` calls all the public API methods on the given +// `ptr` and checks that the calls don't crash and, where applicable, return +// value-initialized values. + +void ExpectAllMethodsAreNoOps(CollectionReference* ptr) { + EXPECT_EQ(*ptr, CollectionReference()); + ExpectCopyableAndMoveable(ptr); + ExpectEqualityToWork(ptr); + + ExpectAllMethodsAreNoOps(static_cast(ptr)); + + EXPECT_EQ(ptr->id(), ""); + EXPECT_EQ(ptr->path(), ""); + + EXPECT_EQ(ptr->Document(), DocumentReference()); + EXPECT_EQ(ptr->Document("foo"), DocumentReference()); + EXPECT_EQ(ptr->Document(std::string("foo")), DocumentReference()); + + EXPECT_EQ(ptr->Add(MapFieldValue()), FailedFuture()); +} + +void ExpectAllMethodsAreNoOps(DocumentChange* ptr) { + // TODO(b/137966104): implement == on `DocumentChange` + // ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_EQ(ptr->type(), DocumentChange::Type()); + // TODO(b/137966104): implement == on `DocumentSnapshot` + EXPECT_NO_THROW(ptr->document()); + EXPECT_EQ(ptr->old_index(), 0); + EXPECT_EQ(ptr->new_index(), 0); +} + +void ExpectAllMethodsAreNoOps(DocumentReference* ptr) { + EXPECT_FALSE(ptr->is_valid()); + + ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + ExpectNullFirestore(ptr); + + EXPECT_EQ(ptr->ToString(), "DocumentReference(invalid)"); + + EXPECT_EQ(ptr->id(), ""); + EXPECT_EQ(ptr->path(), ""); + + EXPECT_EQ(ptr->Parent(), CollectionReference()); + EXPECT_EQ(ptr->Collection("foo"), CollectionReference()); + EXPECT_EQ(ptr->Collection(std::string("foo")), CollectionReference()); + + EXPECT_EQ(ptr->Get(), FailedFuture()); + + EXPECT_EQ(ptr->Set(MapFieldValue()), FailedFuture()); + + EXPECT_EQ(ptr->Update(MapFieldValue()), FailedFuture()); + EXPECT_EQ(ptr->Update(MapFieldPathValue()), FailedFuture()); + + EXPECT_EQ(ptr->Delete(), FailedFuture()); + +#if defined(FIREBASE_USE_STD_FUNCTION) + EXPECT_NO_THROW( + ptr->AddSnapshotListener([](const DocumentSnapshot&, Error) {})); +#else + EXPECT_NO_THROW(ptr->AddSnapshotListener(nullptr)); +#endif +} + +void ExpectAllMethodsAreNoOps(DocumentSnapshot* ptr) { + // TODO(b/137966104): implement == on `DocumentSnapshot` + // ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_EQ(ptr->ToString(), "DocumentSnapshot(invalid)"); + + EXPECT_EQ(ptr->id(), ""); + EXPECT_FALSE(ptr->exists()); + + EXPECT_EQ(ptr->reference(), DocumentReference()); + // TODO(b/137966104): implement == on `SnapshotMetadata` + EXPECT_NO_THROW(ptr->metadata()); + + EXPECT_EQ(ptr->GetData(), MapFieldValue()); + + EXPECT_EQ(ptr->Get("foo"), FieldValue()); + EXPECT_EQ(ptr->Get(std::string("foo")), FieldValue()); + EXPECT_EQ(ptr->Get(FieldPath{"foo"}), FieldValue()); +} + +void ExpectAllMethodsAreNoOps(FieldValue* ptr) { + ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_FALSE(ptr->is_valid()); + // FieldValue doesn't have a separate "invalid" type in its enum. + EXPECT_TRUE(ptr->is_null()); + + EXPECT_EQ(ptr->type(), FieldValue::Type()); + + EXPECT_FALSE(ptr->is_boolean()); + EXPECT_FALSE(ptr->is_integer()); + EXPECT_FALSE(ptr->is_double()); + EXPECT_FALSE(ptr->is_timestamp()); + EXPECT_FALSE(ptr->is_string()); + EXPECT_FALSE(ptr->is_blob()); + EXPECT_FALSE(ptr->is_reference()); + EXPECT_FALSE(ptr->is_geo_point()); + EXPECT_FALSE(ptr->is_array()); + EXPECT_FALSE(ptr->is_map()); + + EXPECT_EQ(ptr->boolean_value(), false); + EXPECT_EQ(ptr->integer_value(), 0); + EXPECT_EQ(ptr->double_value(), 0); + EXPECT_EQ(ptr->timestamp_value(), Timestamp()); + EXPECT_EQ(ptr->string_value(), ""); + EXPECT_EQ(ptr->blob_value(), nullptr); + EXPECT_EQ(ptr->reference_value(), DocumentReference()); + EXPECT_EQ(ptr->geo_point_value(), GeoPoint()); + EXPECT_TRUE(ptr->array_value().empty()); + EXPECT_TRUE(ptr->map_value().empty()); +} + +void ExpectAllMethodsAreNoOps(ListenerRegistration* ptr) { + // `ListenerRegistration` isn't equality comparable. + ExpectCopyableAndMoveable(ptr); + + EXPECT_NO_THROW(ptr->Remove()); +} + +void ExpectAllMethodsAreNoOps(Query* ptr) { + ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + ExpectNullFirestore(ptr); + + EXPECT_EQ(ptr->WhereEqualTo("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereEqualTo(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->WhereLessThan("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereLessThan(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->WhereLessThanOrEqualTo("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereLessThanOrEqualTo(FieldPath{"foo"}, FieldValue()), + Query()); + + EXPECT_EQ(ptr->WhereGreaterThan("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereGreaterThan(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->WhereGreaterThanOrEqualTo("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereGreaterThanOrEqualTo(FieldPath{"foo"}, FieldValue()), + Query()); + + EXPECT_EQ(ptr->WhereArrayContains("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereArrayContains(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->OrderBy("foo"), Query()); + EXPECT_EQ(ptr->OrderBy(FieldPath{"foo"}), Query()); + + EXPECT_EQ(ptr->Limit(123), Query()); + + EXPECT_EQ(ptr->StartAt(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->StartAt(std::vector()), Query()); + + EXPECT_EQ(ptr->StartAfter(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->StartAfter(std::vector()), Query()); + + EXPECT_EQ(ptr->EndBefore(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->EndBefore(std::vector()), Query()); + + EXPECT_EQ(ptr->EndAt(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->EndAt(std::vector()), Query()); + + EXPECT_EQ(ptr->Get(), FailedFuture()); + + EXPECT_EQ(ptr->Get(), FailedFuture()); + +#if defined(FIREBASE_USE_STD_FUNCTION) + EXPECT_NO_THROW(ptr->AddSnapshotListener([](const QuerySnapshot&, Error) {})); +#else + EXPECT_NO_THROW(ptr->AddSnapshotListener(nullptr)); +#endif +} + +void ExpectAllMethodsAreNoOps(QuerySnapshot* ptr) { + // TODO(b/137966104): implement == on `QuerySnapshot` + // ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_EQ(ptr->query(), Query()); + + // TODO(b/137966104): implement == on `SnapshotMetadata` + EXPECT_NO_THROW(ptr->metadata()); + + EXPECT_TRUE(ptr->DocumentChanges().empty()); + EXPECT_TRUE(ptr->documents().empty()); + EXPECT_TRUE(ptr->empty()); + EXPECT_EQ(ptr->size(), 0); +} + +void ExpectAllMethodsAreNoOps(WriteBatch* ptr) { + // `WriteBatch` isn't equality comparable. + ExpectCopyableAndMoveable(ptr); + + EXPECT_NO_THROW(ptr->Set(DocumentReference(), MapFieldValue())); + + EXPECT_NO_THROW(ptr->Update(DocumentReference(), MapFieldValue())); + EXPECT_NO_THROW(ptr->Update(DocumentReference(), MapFieldPathValue())); + + EXPECT_NO_THROW(ptr->Delete(DocumentReference())); + + EXPECT_EQ(ptr->Commit(), FailedFuture()); +} + +using CleanupTest = FirestoreIntegrationTest; + +TEST_F(CleanupTest, CollectionReferenceIsBlankAfterCleanup) { + { + CollectionReference default_constructed; + SCOPED_TRACE("CollectionReference.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + CollectionReference col = Collection(); + DeleteFirestore(); + SCOPED_TRACE("CollectionReference.AfterCleanup"); + ExpectAllMethodsAreNoOps(&col); +} + +TEST_F(CleanupTest, DocumentChangeIsBlankAfterCleanup) { + { + DocumentChange default_constructed; + SCOPED_TRACE("DocumentChange.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + CollectionReference col = Collection("col"); + DocumentReference doc = col.Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + + QuerySnapshot snap = ReadDocuments(col); + auto changes = snap.DocumentChanges(); + ASSERT_EQ(changes.size(), 1); + DocumentChange& change = changes.front(); + + DeleteFirestore(); + SCOPED_TRACE("DocumentChange.AfterCleanup"); + ExpectAllMethodsAreNoOps(&change); +} + +TEST_F(CleanupTest, DocumentReferenceIsBlankAfterCleanup) { + { + DocumentReference default_constructed; + SCOPED_TRACE("DocumentReference.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + DeleteFirestore(); + SCOPED_TRACE("DocumentReference.AfterCleanup"); + ExpectAllMethodsAreNoOps(&doc); +} + +TEST_F(CleanupTest, DocumentSnapshotIsBlankAfterCleanup) { + { + DocumentSnapshot default_constructed; + SCOPED_TRACE("DocumentSnapshot.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + DocumentSnapshot snap = ReadDocument(doc); + + DeleteFirestore(); + SCOPED_TRACE("DocumentSnapshot.AfterCleanup"); + ExpectAllMethodsAreNoOps(&snap); +} + +TEST_F(CleanupTest, FieldValueIsBlankAfterCleanup) { + { + FieldValue default_constructed; + SCOPED_TRACE("FieldValue.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}, + {"ref", FieldValue::Reference(doc)}}); + DocumentSnapshot snap = ReadDocument(doc); + + FieldValue str_value = snap.Get("foo"); + EXPECT_TRUE(str_value.is_valid()); + EXPECT_TRUE(str_value.is_string()); + + FieldValue ref_value = snap.Get("ref"); + EXPECT_TRUE(ref_value.is_valid()); + EXPECT_TRUE(ref_value.is_reference()); + + DeleteFirestore(); + // `FieldValue`s are not cleaned up, because they are owned by the user and + // stay valid after Firestore has shut down. + EXPECT_TRUE(str_value.is_valid()); + EXPECT_TRUE(str_value.is_string()); + EXPECT_EQ(str_value.string_value(), "bar"); + + // However, need to make sure that in a reference value, the reference was + // cleaned up. + EXPECT_TRUE(ref_value.is_valid()); + EXPECT_TRUE(ref_value.is_reference()); + DocumentReference ref_after_cleanup = ref_value.reference_value(); + SCOPED_TRACE("FieldValue.AfterCleanup"); + ExpectAllMethodsAreNoOps(&ref_after_cleanup); +} + +// Note: `Firestore` is not default-constructible, and it is deleted immediately +// after cleanup. Thus, there is no case where a user could be accessing +// a "blank" Firestore instance. + +#if defined(FIREBASE_USE_STD_FUNCTION) +TEST_F(CleanupTest, ListenerRegistrationIsBlankAfterCleanup) { + { + ListenerRegistration default_constructed; + SCOPED_TRACE("ListenerRegistration.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + ListenerRegistration reg = + doc.AddSnapshotListener([](const DocumentSnapshot&, Error) {}); + DeleteFirestore(); + SCOPED_TRACE("ListenerRegistration.AfterCleanup"); + ExpectAllMethodsAreNoOps(®); +} +#endif + +// Note: `Query` cleanup is tested as part of `CollectionReference` cleanup +// (`CollectionReference` is derived from `Query`). + +TEST_F(CleanupTest, QuerySnapshotIsBlankAfterCleanup) { + { + QuerySnapshot default_constructed; + SCOPED_TRACE("QuerySnapshot.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + CollectionReference col = Collection("col"); + DocumentReference doc = col.Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + + QuerySnapshot snap = ReadDocuments(col); + EXPECT_EQ(snap.size(), 1); + + DeleteFirestore(); + SCOPED_TRACE("QuerySnapshot.AfterCleanup"); + ExpectAllMethodsAreNoOps(&snap); +} + +// Note: `Transaction` is uncopyable and not default constructible, and storing +// a pointer to a `Transaction` is not valid in general, because the object will +// be destroyed as soon as the transaction is finished. Thus, there is no valid +// case where a user could be accessing a "blank" transaction. + +TEST_F(CleanupTest, WriteBatchIsBlankAfterCleanup) { + { + WriteBatch default_constructed; + SCOPED_TRACE("WriteBatch.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + WriteBatch batch = firestore()->batch(); + DeleteFirestore(); + SCOPED_TRACE("WriteBatch.AfterCleanup"); + ExpectAllMethodsAreNoOps(&batch); +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/collection_reference_test.cc b/firestore/src/tests/collection_reference_test.cc new file mode 100644 index 0000000000..1c700497b9 --- /dev/null +++ b/firestore/src/tests/collection_reference_test.cc @@ -0,0 +1,34 @@ +#include + +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/collection_reference_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/collection_reference_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using CollectionReferenceTest = testing::Test; + +TEST_F(CollectionReferenceTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(CollectionReferenceTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/cursor_test.cc b/firestore/src/tests/cursor_test.cc new file mode 100644 index 0000000000..de9e6857fe --- /dev/null +++ b/firestore/src/tests/cursor_test.cc @@ -0,0 +1,278 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRCursorTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/CursorTest.java +// The iOS test names start with the mandatory test prefix while Android test +// names do not. Here we use the Android test names. + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, CanPageThroughItems) { + CollectionReference collection = + Collection({{"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}, + {"c", {{"v", FieldValue::String("c")}}}, + {"d", {{"v", FieldValue::String("d")}}}, + {"e", {{"v", FieldValue::String("e")}}}, + {"f", {{"v", FieldValue::String("f")}}}}); + QuerySnapshot snapshot = ReadDocuments(collection.Limit(2)); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(snapshot)); + + DocumentSnapshot last_doc = snapshot.documents()[1]; + snapshot = ReadDocuments(collection.Limit(3).StartAfter(last_doc)); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("c")}}, + {{"v", FieldValue::String("d")}}, + {{"v", FieldValue::String("e")}}}), + QuerySnapshotToValues(snapshot)); + + last_doc = snapshot.documents()[2]; + snapshot = ReadDocuments(collection.Limit(1).StartAfter(last_doc)); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("f")}}}), + QuerySnapshotToValues(snapshot)); + + last_doc = snapshot.documents()[0]; + snapshot = ReadDocuments(collection.Limit(3).StartAfter(last_doc)); + EXPECT_EQ(std::vector{}, QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, CanBeCreatedFromDocuments) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Double(2.0)}}}, + {"e", + {{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}}, + // should not show up + {"f", + {{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + Query query = collection.OrderBy("sort"); + DocumentSnapshot snapshot = ReadDocument(collection.Document("c")); + + EXPECT_TRUE(snapshot.exists()); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("c")}, + {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("d")}, + {"sort", FieldValue::Double(2.0)}}}), + QuerySnapshotToValues(ReadDocuments(query.StartAt(snapshot)))); + + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}, + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}, + {{"k", FieldValue::String("b")}, + {"sort", FieldValue::Double(2.0)}}}), + QuerySnapshotToValues(ReadDocuments(query.EndBefore(snapshot)))); +} + +TEST_F(FirestoreIntegrationTest, CanBeCreatedFromValues) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Double(2.0)}}}, + {"e", + {{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}}, + // should not show up + {"f", + {{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + Query query = collection.OrderBy("sort"); + + QuerySnapshot snapshot = ReadDocuments( + query.StartAt(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("d")}, + {"sort", FieldValue::Double(2.0)}}}), + QuerySnapshotToValues(snapshot)); + + snapshot = ReadDocuments( + query.EndBefore(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Double(0.0)}}, + {{"k", FieldValue::String("a")}, + {"sort", FieldValue::Double(1.0)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, CanBeCreatedUsingDocumentId) { + std::map docs = { + {"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}, + {"c", {{"v", FieldValue::String("c")}}}, + {"d", {{"v", FieldValue::String("d")}}}, + {"e", {{"v", FieldValue::String("e")}}}}; + + CollectionReference writer = CachedFirestore("writer") + ->Collection("parent-collection") + .Document() + .Collection("sub-collection"); + WriteDocuments(writer, docs); + + CollectionReference reader = + CachedFirestore("reader")->Collection(writer.path()); + QuerySnapshot snapshot = ReadDocuments( + reader.OrderBy(FieldPath::DocumentId()) + .StartAt(std::vector({FieldValue::String("b")})) + .EndBefore(std::vector({FieldValue::String("d")}))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("b")}}, + {{"v", FieldValue::String("c")}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, CanBeUsedWithReferenceValues) { + Firestore* db = firestore(); + std::map docs = { + {"a", + {{"k", FieldValue::String("1a")}, + {"ref", FieldValue::Reference(db->Collection("1").Document("a"))}}}, + {"b", + {{"k", FieldValue::String("1b")}, + {"ref", FieldValue::Reference(db->Collection("1").Document("b"))}}}, + {"c", + {{"k", FieldValue::String("2a")}, + {"ref", FieldValue::Reference(db->Collection("2").Document("a"))}}}, + {"d", + {{"k", FieldValue::String("2b")}, + {"ref", FieldValue::Reference(db->Collection("2").Document("b"))}}}, + {"e", + {{"k", FieldValue::String("3a")}, + {"ref", FieldValue::Reference(db->Collection("3").Document("a"))}}}}; + + CollectionReference collection = Collection(docs); + + QuerySnapshot snapshot = ReadDocuments( + collection.OrderBy("ref") + .StartAfter(std::vector( + {FieldValue::Reference(db->Collection("1").Document("a"))})) + .EndAt(std::vector( + {FieldValue::Reference(db->Collection("2").Document("b"))}))); + + std::vector results; + for (const DocumentSnapshot& doc : snapshot.documents()) { + results.push_back(doc.Get("k").string_value()); + } + EXPECT_EQ(std::vector({"1b", "2a", "2b"}), results); +} + +TEST_F(FirestoreIntegrationTest, CanBeUsedInDescendingQueries) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Double(3.0)}}}, + {"e", + {{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}}, + // should not show up + {"f", + {{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + Query query = + collection.OrderBy("sort", Query::Direction::kDescending) + .OrderBy(FieldPath::DocumentId(), Query::Direction::kDescending); + + QuerySnapshot snapshot = ReadDocuments( + query.StartAt(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}, + {{"k", FieldValue::String("e")}, + {"sort", FieldValue::Double(0.0)}}}), + QuerySnapshotToValues(snapshot)); + + snapshot = ReadDocuments( + query.EndBefore(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("d")}, + {"sort", FieldValue::Double(3.0)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TimestampsCanBePassedToQueriesAsLimits) { + CollectionReference collection = + Collection({{"a", {{"timestamp", FieldValue::Timestamp({100, 2000})}}}, + {"b", {{"timestamp", FieldValue::Timestamp({100, 5000})}}}, + {"c", {{"timestamp", FieldValue::Timestamp({100, 3000})}}}, + {"d", {{"timestamp", FieldValue::Timestamp({100, 1000})}}}, + // Number of nanoseconds deliberately repeated. + {"e", {{"timestamp", FieldValue::Timestamp({100, 5000})}}}, + {"f", {{"timestamp", FieldValue::Timestamp({100, 4000})}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.OrderBy("timestamp") + .StartAfter( + std::vector({FieldValue::Timestamp({100, 2000})})) + .EndAt( + std::vector({FieldValue::Timestamp({100, 5000})}))); + EXPECT_EQ(std::vector({"c", "f", "b", "e"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TimestampsCanBePassedToQueriesInWhereClause) { + CollectionReference collection = + Collection({{"a", {{"timestamp", FieldValue::Timestamp({100, 7000})}}}, + {"b", {{"timestamp", FieldValue::Timestamp({100, 4000})}}}, + {"c", {{"timestamp", FieldValue::Timestamp({100, 8000})}}}, + {"d", {{"timestamp", FieldValue::Timestamp({100, 5000})}}}, + {"e", {{"timestamp", FieldValue::Timestamp({100, 6000})}}}}); + + QuerySnapshot snapshot = ReadDocuments( + collection + .WhereGreaterThanOrEqualTo("timestamp", + FieldValue::Timestamp({100, 5000})) + .WhereLessThan("timestamp", FieldValue::Timestamp({100, 8000}))); + EXPECT_EQ(std::vector({"d", "e", "a"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TimestampsAreTruncatedToMicroseconds) { + const FieldValue nanos = FieldValue::Timestamp({0, 123456789}); + const FieldValue micros = FieldValue::Timestamp({0, 123456000}); + const FieldValue millis = FieldValue::Timestamp({0, 123000000}); + CollectionReference collection = Collection({{"a", {{"timestamp", nanos}}}}); + + QuerySnapshot snapshot = + ReadDocuments(collection.WhereEqualTo("timestamp", nanos)); + EXPECT_EQ(1, QuerySnapshotToValues(snapshot).size()); + + // Because Timestamp should have been truncated to microseconds, the + // microsecond timestamp should be considered equal to the nanosecond one. + snapshot = ReadDocuments(collection.WhereEqualTo("timestamp", micros)); + EXPECT_EQ(1, QuerySnapshotToValues(snapshot).size()); + + // The truncation is just to the microseconds, however, so the millisecond + // timestamp should be treated as different and thus the query should return + // no results. + snapshot = ReadDocuments(collection.WhereEqualTo("timestamp", millis)); + EXPECT_TRUE(QuerySnapshotToValues(snapshot).empty()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/document_change_test.cc b/firestore/src/tests/document_change_test.cc new file mode 100644 index 0000000000..20d4f902e0 --- /dev/null +++ b/firestore/src/tests/document_change_test.cc @@ -0,0 +1,31 @@ +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#if defined(__ANDROID__) +#include "firestore/src/android/document_change_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/document_change_stub.h" +#endif // defined(__ANDROID__) + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using DocumentChangeTest = testing::Test; + +TEST_F(DocumentChangeTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(DocumentChangeTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/document_reference_test.cc b/firestore/src/tests/document_reference_test.cc new file mode 100644 index 0000000000..64e66f634c --- /dev/null +++ b/firestore/src/tests/document_reference_test.cc @@ -0,0 +1,34 @@ +#include + +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/document_reference_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/document_reference_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using DocumentReferenceTest = testing::Test; + +TEST_F(DocumentReferenceTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(DocumentReferenceTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/document_snapshot_test.cc b/firestore/src/tests/document_snapshot_test.cc new file mode 100644 index 0000000000..5c995b734b --- /dev/null +++ b/firestore/src/tests/document_snapshot_test.cc @@ -0,0 +1,35 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/document_snapshot_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/stub/document_snapshot_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using DocumentSnapshotTest = testing::Test; + +TEST_F(DocumentSnapshotTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(DocumentSnapshotTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/field_value_test.cc b/firestore/src/tests/field_value_test.cc new file mode 100644 index 0000000000..aaf382ea92 --- /dev/null +++ b/firestore/src/tests/field_value_test.cc @@ -0,0 +1,331 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#if defined(__ANDROID__) +#include "firestore/src/android/field_value_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/field_value_stub.h" +#else +#include "firestore/src/ios/field_value_ios.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +using Type = FieldValue::Type; +using FieldValueTest = testing::Test; + +// Sanity test for stubs +TEST_F(FirestoreIntegrationTest, TestFieldValueTypes) { + ASSERT_NO_THROW({ + FieldValue::Null(); + FieldValue::Boolean(true); + FieldValue::Integer(123L); + FieldValue::Double(3.1415926); + FieldValue::Timestamp({12345, 54321}); + FieldValue::String("hello"); + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + FieldValue::Blob(blob, sizeof(blob)); + FieldValue::GeoPoint({43, 80}); + FieldValue::Array(std::vector{FieldValue::Null()}); + FieldValue::Map(MapFieldValue{{"Null", FieldValue::Null()}}); + FieldValue::Delete(); + FieldValue::ServerTimestamp(); + FieldValue::ArrayUnion(std::vector{FieldValue::Null()}); + FieldValue::ArrayRemove(std::vector{FieldValue::Null()}); + }); +} + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +TEST_F(FieldValueTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(FieldValueTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +#if !defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestNullType) { + FieldValue value = FieldValue::Null(); + EXPECT_EQ(Type::kNull, value.type()); +} + +TEST_F(FirestoreIntegrationTest, TestBooleanType) { + FieldValue value = FieldValue::Boolean(true); + EXPECT_EQ(Type::kBoolean, value.type()); + EXPECT_EQ(true, value.boolean_value()); +} + +TEST_F(FirestoreIntegrationTest, TestIntegerType) { + FieldValue value = FieldValue::Integer(123); + EXPECT_EQ(Type::kInteger, value.type()); + EXPECT_EQ(123, value.integer_value()); +} + +TEST_F(FirestoreIntegrationTest, TestDoubleType) { + FieldValue value = FieldValue::Double(3.1415926); + EXPECT_EQ(Type::kDouble, value.type()); + EXPECT_EQ(3.1415926, value.double_value()); +} + +TEST_F(FirestoreIntegrationTest, TestTimestampType) { + FieldValue value = FieldValue::Timestamp({12345, 54321}); + EXPECT_EQ(Type::kTimestamp, value.type()); + EXPECT_EQ(Timestamp(12345, 54321), value.timestamp_value()); +} + +TEST_F(FirestoreIntegrationTest, TestStringType) { + FieldValue value = FieldValue::String("hello"); + EXPECT_EQ(Type::kString, value.type()); + EXPECT_STREQ("hello", value.string_value().c_str()); +} + +TEST_F(FirestoreIntegrationTest, TestBlobType) { + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + FieldValue value = FieldValue::Blob(blob, sizeof(blob)); + EXPECT_EQ(Type::kBlob, value.type()); + EXPECT_EQ(sizeof(blob), value.blob_size()); + const uint8_t* value_blob = value.blob_value(); + + FieldValue copied(value); + EXPECT_EQ(Type::kBlob, copied.type()); + EXPECT_EQ(sizeof(blob), copied.blob_size()); + const uint8_t* copied_blob = copied.blob_value(); + + for (int i = 0; i < sizeof(blob); ++i) { + EXPECT_EQ(blob[i], value_blob[i]); + EXPECT_EQ(blob[i], copied_blob[i]); + } +} + +TEST_F(FirestoreIntegrationTest, TestReferenceType) { + FieldValue value = FieldValue::Reference(firestore()->Document("foo/bar")); + EXPECT_EQ(Type::kReference, value.type()); + EXPECT_EQ(value.reference_value().path(), "foo/bar"); +} + +TEST_F(FirestoreIntegrationTest, TestGeoPointType) { + FieldValue value = FieldValue::GeoPoint({43, 80}); + EXPECT_EQ(Type::kGeoPoint, value.type()); + EXPECT_EQ(GeoPoint(43, 80), value.geo_point_value()); +} + +TEST_F(FirestoreIntegrationTest, TestArrayType) { + FieldValue value = FieldValue::Array( + {FieldValue::Boolean(true), FieldValue::Integer(123)}); + EXPECT_EQ(Type::kArray, value.type()); + const std::vector& array = value.array_value(); + EXPECT_EQ(2, array.size()); + EXPECT_EQ(true, array[0].boolean_value()); + EXPECT_EQ(123, array[1].integer_value()); +} + +TEST_F(FirestoreIntegrationTest, TestMapType) { + FieldValue value = + FieldValue::Map(MapFieldValue{{"Bool", FieldValue::Boolean(true)}, + {"Int", FieldValue::Integer(123)}}); + EXPECT_EQ(Type::kMap, value.type()); + MapFieldValue map = value.map_value(); + EXPECT_EQ(2, map.size()); + EXPECT_EQ(true, map["Bool"].boolean_value()); + EXPECT_EQ(123, map["Int"].integer_value()); +} + +TEST_F(FirestoreIntegrationTest, TestSentinelType) { + FieldValue delete_value = FieldValue::Delete(); + EXPECT_EQ(Type::kDelete, delete_value.type()); + + FieldValue server_timestamp_value = FieldValue::ServerTimestamp(); + EXPECT_EQ(Type::kServerTimestamp, server_timestamp_value.type()); + + std::vector array = {FieldValue::Boolean(true), + FieldValue::Integer(123)}; + FieldValue array_union = FieldValue::ArrayUnion(array); + EXPECT_EQ(Type::kArrayUnion, array_union.type()); + FieldValue array_remove = FieldValue::ArrayRemove(array); + EXPECT_EQ(Type::kArrayRemove, array_remove.type()); + + FieldValue increment_integer = FieldValue::Increment(1); + EXPECT_EQ(Type::kIncrementInteger, increment_integer.type()); + + FieldValue increment_double = FieldValue::Increment(1.0); + EXPECT_EQ(Type::kIncrementDouble, increment_double.type()); +} + +TEST_F(FirestoreIntegrationTest, TestEquality) { + EXPECT_EQ(FieldValue::Null(), FieldValue::Null()); + EXPECT_EQ(FieldValue::Boolean(true), FieldValue::Boolean(true)); + EXPECT_EQ(FieldValue::Integer(123), FieldValue::Integer(123)); + EXPECT_EQ(FieldValue::Double(456.0), FieldValue::Double(456.0)); + EXPECT_EQ(FieldValue::String("foo"), FieldValue::String("foo")); + + EXPECT_EQ(FieldValue::Timestamp({123, 456}), + FieldValue::Timestamp({123, 456})); + + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + EXPECT_EQ(FieldValue::Blob(blob, sizeof(blob)), + FieldValue::Blob(blob, sizeof(blob))); + + EXPECT_EQ(FieldValue::GeoPoint({43, 80}), FieldValue::GeoPoint({43, 80})); + + EXPECT_EQ( + FieldValue::Array({FieldValue::Integer(3), FieldValue::Double(4.0)}), + FieldValue::Array({FieldValue::Integer(3), FieldValue::Double(4.0)})); + + EXPECT_EQ(FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(3)}}), + FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(3)}})); + + EXPECT_EQ(FieldValue::Delete(), FieldValue::Delete()); + EXPECT_EQ(FieldValue::ServerTimestamp(), FieldValue::ServerTimestamp()); + // TODO(varconst): make this work on Android, or remove the tests below. + // EXPECT_EQ(FieldValue::ArrayUnion({FieldValue::Null()}), + // FieldValue::ArrayUnion({FieldValue::Null()})); + // EXPECT_EQ(FieldValue::ArrayRemove({FieldValue::Null()}), + // FieldValue::ArrayRemove({FieldValue::Null()})); +} + +TEST_F(FirestoreIntegrationTest, TestInequality) { + EXPECT_NE(FieldValue::Boolean(false), FieldValue::Boolean(true)); + EXPECT_NE(FieldValue::Integer(123), FieldValue::Integer(456)); + EXPECT_NE(FieldValue::Double(123.0), FieldValue::Double(456.0)); + EXPECT_NE(FieldValue::String("foo"), FieldValue::String("bar")); + + EXPECT_NE(FieldValue::Timestamp({123, 456}), + FieldValue::Timestamp({789, 123})); + + uint8_t blob1[] = "( ͡° ͜ʖ ͡°)"; + uint8_t blob2[] = "___"; + EXPECT_NE(FieldValue::Blob(blob1, sizeof(blob2)), + FieldValue::Blob(blob2, sizeof(blob2))); + + EXPECT_NE(FieldValue::GeoPoint({43, 80}), FieldValue::GeoPoint({12, 34})); + + EXPECT_NE( + FieldValue::Array({FieldValue::Integer(3), FieldValue::Double(4.0)}), + FieldValue::Array({FieldValue::Integer(5), FieldValue::Double(4.0)})); + + EXPECT_NE(FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(3)}}), + FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(4)}})); + + EXPECT_NE(FieldValue::Delete(), FieldValue::ServerTimestamp()); + EXPECT_NE(FieldValue::ArrayUnion({FieldValue::Null()}), + FieldValue::ArrayUnion({FieldValue::Boolean(false)})); + EXPECT_NE(FieldValue::ArrayRemove({FieldValue::Null()}), + FieldValue::ArrayRemove({FieldValue::Boolean(false)})); +} + +TEST_F(FirestoreIntegrationTest, TestInequalityDueToDifferentTypes) { + EXPECT_NE(FieldValue::Null(), FieldValue::Delete()); + EXPECT_NE(FieldValue::Integer(1), FieldValue::Boolean(true)); + EXPECT_NE(FieldValue::Integer(123), FieldValue::Double(123)); + EXPECT_NE(FieldValue::ArrayUnion({FieldValue::Null()}), + FieldValue::ArrayRemove({FieldValue::Null()})); + EXPECT_NE(FieldValue::Array({FieldValue::Null()}), + FieldValue::ArrayRemove({FieldValue::Null()})); + // Fully exhaustive check seems overkill, just check the types that are known + // to have the same (or very similar) representation. +} + +TEST_F(FirestoreIntegrationTest, TestToString) { + EXPECT_EQ("", FieldValue().ToString()); + + EXPECT_EQ("null", FieldValue::Null().ToString()); + EXPECT_EQ("true", FieldValue::Boolean(true).ToString()); + EXPECT_EQ("123", FieldValue::Integer(123L).ToString()); + EXPECT_EQ("3.14", FieldValue::Double(3.14).ToString()); + EXPECT_EQ("Timestamp(seconds=12345, nanoseconds=54321)", + FieldValue::Timestamp({12345, 54321}).ToString()); + EXPECT_EQ("'hello'", FieldValue::String("hello").ToString()); + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + EXPECT_EQ("Blob(28 20 cd a1 c2 b0 20 cd 9c ca 96 20 cd a1 c2 b0 29 00)", + FieldValue::Blob(blob, sizeof(blob)).ToString()); + EXPECT_EQ("GeoPoint(latitude=43, longitude=80)", + FieldValue::GeoPoint({43, 80}).ToString()); + + EXPECT_EQ("DocumentReference(invalid)", FieldValue::Reference({}).ToString()); + + EXPECT_EQ("[]", FieldValue::Array({}).ToString()); + EXPECT_EQ("[null]", FieldValue::Array({FieldValue::Null()}).ToString()); + EXPECT_EQ("[null, true, 1]", + FieldValue::Array({FieldValue::Null(), FieldValue::Boolean(true), + FieldValue::Integer(1)}) + .ToString()); + // TODO(b/150016438): uncomment this case (fails on Android). + // EXPECT_EQ("[]", FieldValue::Array({FieldValue()}).ToString()); + + EXPECT_EQ("{}", FieldValue::Map({}).ToString()); + // TODO(b/150016438): uncomment this case (fails on Android). + // EXPECT_EQ("{bad: }", FieldValue::Map({ + // {"bad", + // FieldValue()}, + // }) + // .ToString()); + EXPECT_EQ("{Null: null}", FieldValue::Map({ + {"Null", FieldValue::Null()}, + }) + .ToString()); + // Note: because the map is unordered, it's hard to check the case where a map + // has more than one element. + + EXPECT_EQ("FieldValue::Delete()", FieldValue::Delete().ToString()); + EXPECT_EQ("FieldValue::ServerTimestamp()", + FieldValue::ServerTimestamp().ToString()); + EXPECT_EQ("FieldValue::ArrayUnion()", + FieldValue::ArrayUnion({FieldValue::Null()}).ToString()); + EXPECT_EQ("FieldValue::ArrayRemove()", + FieldValue::ArrayRemove({FieldValue::Null()}).ToString()); + + EXPECT_EQ("FieldValue::Increment()", FieldValue::Increment(1).ToString()); + EXPECT_EQ("FieldValue::Increment()", FieldValue::Increment(1.0).ToString()); +} + +TEST_F(FirestoreIntegrationTest, TestIncrementChoosesTheCorrectType) { + // Signed integers + // NOLINTNEXTLINE -- exact integer width doesn't matter. + short foo = 1; + EXPECT_EQ(FieldValue::Increment(foo).type(), Type::kIncrementInteger); + EXPECT_EQ(FieldValue::Increment(1).type(), Type::kIncrementInteger); + EXPECT_EQ(FieldValue::Increment(1L).type(), Type::kIncrementInteger); + // Note: using `long long` syntax to avoid go/lsc-long-long-literal. + // NOLINTNEXTLINE -- exact integer width doesn't matter. + long long llfoo = 1; + EXPECT_EQ(FieldValue::Increment(llfoo).type(), Type::kIncrementInteger); + + // Unsigned integers + // NOLINTNEXTLINE -- exact integer width doesn't matter. + unsigned short ufoo = 1; + EXPECT_EQ(FieldValue::Increment(ufoo).type(), Type::kIncrementInteger); + EXPECT_EQ(FieldValue::Increment(1U).type(), Type::kIncrementInteger); + + // Floating point + EXPECT_EQ(FieldValue::Increment(1.0f).type(), Type::kIncrementDouble); + EXPECT_EQ(FieldValue::Increment(1.0).type(), Type::kIncrementDouble); + + // The statements below shouldn't compile (uncomment to check). + + // Types that would lead to truncation: + // EXPECT_EQ(FieldValue::Increment(1UL).type(), Type::kIncrementInteger); + // unsigned long long ullfoo = 1; + // EXPECT_EQ(FieldValue::Increment(ullfoo).type(), Type::kIncrementInteger); + // EXPECT_EQ(FieldValue::Increment(1.0L).type(), Type::kIncrementDouble); + + // Inapplicable types: + // EXPECT_EQ(FieldValue::Increment(true).type(), Type::kIncrementInteger); + // EXPECT_EQ(FieldValue::Increment('a').type(), Type::kIncrementInteger); + // EXPECT_EQ(FieldValue::Increment("abc").type(), Type::kIncrementInteger); +} + +#endif // !defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/fields_test.cc b/firestore/src/tests/fields_test.cc new file mode 100644 index 0000000000..76e6c3bcce --- /dev/null +++ b/firestore/src/tests/fields_test.cc @@ -0,0 +1,229 @@ +#include +#include +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/FieldsTest.java +// except we do not port the tests for legacy timestamp behavior. C++ SDK does +// not support the legacy timestamp behavior. + +namespace firebase { +namespace firestore { + +class FieldsTest : public FirestoreIntegrationTest { + protected: + /** + * Creates test data with nested fields. + */ + MapFieldValue NestedData(int number) { + char buffer[32]; + MapFieldValue result; + + snprintf(buffer, sizeof(buffer), "room %d", number); + result["name"] = FieldValue::String(buffer); + + MapFieldValue nested; + nested["createdAt"] = FieldValue::Integer(number); + MapFieldValue deep_nested; + snprintf(buffer, sizeof(buffer), "deep-field-%d", number); + deep_nested["field"] = FieldValue::String(buffer); + nested["deep"] = FieldValue::Map(deep_nested); + result["metadata"] = FieldValue::Map(nested); + + return result; + } + + /** + * Creates test data with special characters in field names. Datastore + * currently prohibits mixing nested data with special characters so tests + * that use this data must be separate. + */ + MapFieldValue DottedData(int number) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "field %d", number); + + return {{"a", FieldValue::String(buffer)}, + {"b.dot", FieldValue::Integer(number)}, + {"c\\slash", FieldValue::Integer(number)}}; + } + + /** + * Creates test data with Timestamp. + */ + MapFieldValue DataWithTimestamp(Timestamp timestamp) { + return { + {"timestamp", FieldValue::Timestamp(timestamp)}, + {"nested", + FieldValue::Map({{"timestamp2", FieldValue::Timestamp(timestamp)}})}}; + } +}; + +TEST_F(FieldsTest, TestNestedFieldsCanBeWrittenWithSet) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + EXPECT_THAT(ReadDocument(doc).GetData(), testing::ContainerEq(NestedData(1))); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeReadDirectly) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + DocumentSnapshot snapshot = ReadDocument(doc); + + MapFieldValue expected = NestedData(1); + EXPECT_EQ(expected["name"].string_value(), + snapshot.Get("name").string_value()); + EXPECT_EQ(expected["metadata"].map_value(), + snapshot.Get("metadata").map_value()); + EXPECT_EQ(expected["metadata"] + .map_value()["deep"] + .map_value()["field"] + .string_value(), + snapshot.Get("metadata.deep.field").string_value()); + EXPECT_FALSE(snapshot.Get("metadata.nofield").is_valid()); + EXPECT_FALSE(snapshot.Get("nometadata.nofield").is_valid()); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeReadDirectlyViaFieldPath) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + DocumentSnapshot snapshot = ReadDocument(doc); + + MapFieldValue expected = NestedData(1); + EXPECT_EQ(expected["name"].string_value(), + snapshot.Get(FieldPath{"name"}).string_value()); + EXPECT_EQ(expected["metadata"].map_value(), + snapshot.Get(FieldPath{"metadata"}).map_value()); + EXPECT_EQ( + expected["metadata"] + .map_value()["deep"] + .map_value()["field"] + .string_value(), + snapshot.Get(FieldPath{"metadata", "deep", "field"}).string_value()); + EXPECT_FALSE(snapshot.Get(FieldPath{"metadata", "nofield"}).is_valid()); + EXPECT_FALSE(snapshot.Get(FieldPath{"nometadata", "nofield"}).is_valid()); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeUpdated) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + UpdateDocument( + doc, MapFieldValue{{"metadata.deep.field", FieldValue::Integer(100)}, + {"metadata.added", FieldValue::Integer(200)}}); + EXPECT_THAT(ReadDocument(doc).GetData(), + testing::ContainerEq(MapFieldValue( + {{"name", FieldValue::String("room 1")}, + {"metadata", + FieldValue::Map( + {{"createdAt", FieldValue::Integer(1)}, + {"deep", FieldValue::Map( + {{"field", FieldValue::Integer(100)}})}, + {"added", FieldValue::Integer(200)}})}}))); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeUsedInQueryFilters) { + CollectionReference collection = Collection( + {{"1", NestedData(300)}, {"2", NestedData(100)}, {"3", NestedData(200)}}); + QuerySnapshot snapshot = ReadDocuments(collection.WhereGreaterThanOrEqualTo( + "metadata.createdAt", FieldValue::Integer(200))); + // inequality adds implicit sort on field + EXPECT_THAT(QuerySnapshotToValues(snapshot), + testing::ElementsAre(NestedData(200), NestedData(300))); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeUsedInOrderBy) { + CollectionReference collection = Collection( + {{"1", NestedData(300)}, {"2", NestedData(100)}, {"3", NestedData(200)}}); + QuerySnapshot snapshot = + ReadDocuments(collection.OrderBy("metadata.createdAt")); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(NestedData(100), NestedData(200), NestedData(300))); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeWrittenWithSet) { + DocumentReference doc = Document(); + WriteDocument(doc, DottedData(1)); + EXPECT_EQ(DottedData(1), ReadDocument(doc).GetData()); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeReadDirectly) { + DocumentReference doc = Document(); + WriteDocument(doc, DottedData(1)); + DocumentSnapshot snapshot = ReadDocument(doc); + + MapFieldValue expected = DottedData(1); + EXPECT_EQ(expected["a"].string_value(), snapshot.Get("a").string_value()); + EXPECT_EQ(expected["b.dot"].integer_value(), + snapshot.GetData()["b.dot"].integer_value()); + EXPECT_EQ(expected["c\\slash"].integer_value(), + snapshot.GetData()["c\\slash"].integer_value()); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeUpdated) { + DocumentReference doc = Document(); + WriteDocument(doc, DottedData(1)); + UpdateDocument(doc, MapFieldPathValue{ + {FieldPath{"b.dot"}, FieldValue::Integer(100)}, + {FieldPath{"c\\slash"}, FieldValue::Integer(200)}}); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(ReadDocument(doc).GetData(), + testing::ContainerEq( + MapFieldValue({{"a", FieldValue::String("field 1")}, + {"b.dot", FieldValue::Integer(100)}, + {"c\\slash", FieldValue::Integer(200)}}))); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeUsedInQueryFilters) { + CollectionReference collection = Collection( + {{"1", DottedData(300)}, {"2", DottedData(100)}, {"3", DottedData(200)}}); + QuerySnapshot snapshot = ReadDocuments(collection.WhereGreaterThanOrEqualTo( + FieldPath{"b.dot"}, FieldValue::Integer(200))); + // inequality adds implicit sort on field + EXPECT_THAT(QuerySnapshotToValues(snapshot), + testing::ElementsAre(DottedData(200), DottedData(300))); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeUsedInOrderBy) { + CollectionReference collection = Collection( + {{"1", DottedData(300)}, {"2", DottedData(100)}, {"3", DottedData(200)}}); + QuerySnapshot snapshot = + ReadDocuments(collection.OrderBy(FieldPath{"b.dot"})); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(DottedData(100), DottedData(200), DottedData(300))); +} + +TEST_F(FieldsTest, TestTimestampsInSnapshots) { + Timestamp original_timestamp{100, 123456789}; + // Timestamps are currently truncated to microseconds after being written to + // the database. + Timestamp truncated_timestamp{100, 123456000}; + + DocumentReference doc = Document(); + WriteDocument(doc, DataWithTimestamp(original_timestamp)); + DocumentSnapshot snapshot = ReadDocument(doc); + MapFieldValue data = snapshot.GetData(); + + Timestamp timestamp_from_snapshot = + snapshot.Get("timestamp").timestamp_value(); + Timestamp timestamp_from_data = data["timestamp"].timestamp_value(); + EXPECT_EQ(truncated_timestamp, timestamp_from_data); + EXPECT_EQ(timestamp_from_snapshot, timestamp_from_data); + + timestamp_from_snapshot = snapshot.Get("nested.timestamp2").timestamp_value(); + timestamp_from_data = + data["nested"].map_value()["timestamp2"].timestamp_value(); + EXPECT_EQ(truncated_timestamp, timestamp_from_data); + EXPECT_EQ(timestamp_from_snapshot, timestamp_from_data); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/firestore_integration_test.cc b/firestore/src/tests/firestore_integration_test.cc new file mode 100644 index 0000000000..7462004b94 --- /dev/null +++ b/firestore/src/tests/firestore_integration_test.cc @@ -0,0 +1,219 @@ +#include "firestore/src/tests/firestore_integration_test.h" + +#include +#include +#include + +#include "absl/strings/ascii.h" +#include "Firestore/core/src/util/autoid.h" + +namespace firebase { +namespace firestore { + +namespace { +// name of FirebaseApp to use for bootstrapping data into Firestore. We use a +// non-default app to avoid data ending up in the cache before tests run. +static const char* kBootstrapAppName = "bootstrap"; + +void Release(Firestore* firestore) { + if (firestore == nullptr) { + return; + } + + App* app = firestore->app(); + delete firestore; + delete app; +} + +// Set Firestore up to use Firestore Emulator if it can be found. +void LocateEmulator(Firestore* db) { + // iOS and Android pass emulator address differently, iOS writes it to a + // temp file, but there is no equivalent to `/tmp/` for Android, so it + // uses an environment variable instead. + // TODO(wuandy): See if we can use environment variable for iOS as well? + std::ifstream ifs("/tmp/emulator_address"); + std::stringstream buffer; + buffer << ifs.rdbuf(); + std::string address; + if (ifs.good()) { + address = buffer.str(); + } else if (std::getenv("FIRESTORE_EMULATOR_HOST")) { + address = std::getenv("FIRESTORE_EMULATOR_HOST"); + } + + absl::StripAsciiWhitespace(&address); + if (!address.empty()) { + auto settings = db->settings(); + settings.set_host(address); + // Emulator does not support ssl yet. + settings.set_ssl_enabled(false); + db->set_settings(settings); + } +} + +} // anonymous namespace + +FirestoreIntegrationTest::FirestoreIntegrationTest() { + // Allocate the default Firestore eagerly. + CachedFirestore(kDefaultAppName); + Firestore::set_log_level(LogLevel::kLogLevelDebug); +} + +FirestoreIntegrationTest::~FirestoreIntegrationTest() { + for (auto named_firestore : firestores_) { + Await(named_firestore.second->Terminate()); + Release(named_firestore.second); + firestores_[named_firestore.first] = nullptr; + } +} + +Firestore* FirestoreIntegrationTest::CachedFirestore( + const std::string& name) const { + if (firestores_.count(name) > 0) { + return firestores_[name]; + } + + // Make sure different unit tests don't try to create an app with the same + // name, because it's not supported by `firebase::App` (the default app is an + // exception and will be recreated). + static int counter = 0; + std::string app_name = + name == kDefaultAppName ? name : name + std::to_string(counter++); + Firestore* db = CreateFirestore(app_name); + + firestores_[name] = db; + return db; +} + +Firestore* FirestoreIntegrationTest::CreateFirestore() const { + static int app_number = 0; + std::string app_name = "app_for_testing_"; + app_name += std::to_string(app_number++); + return CreateFirestore(app_name); +} + +Firestore* FirestoreIntegrationTest::CreateFirestore( + const std::string& app_name) const { + App* app = GetApp(app_name.c_str()); + Firestore* db = new Firestore(CreateTestFirestoreInternal(app)); + + LocateEmulator(db); + InitializeFirestore(db); + return db; +} + +void FirestoreIntegrationTest::DeleteFirestore(const std::string& name) { + auto found = firestores_.find(name); + FIREBASE_ASSERT_MESSAGE( + found != firestores_.end(), + "Couldn't find Firestore corresponding to app name '%s'", name.c_str()); + + TerminateAndRelease(found->second); + firestores_.erase(found); +} + +CollectionReference FirestoreIntegrationTest::Collection() const { + return firestore()->Collection(util::CreateAutoId()); +} + +CollectionReference FirestoreIntegrationTest::Collection( + const std::string& name_prefix) const { + return firestore()->Collection(name_prefix + "_" + util::CreateAutoId()); +} + +CollectionReference FirestoreIntegrationTest::Collection( + const std::map& docs) const { + CollectionReference result = Collection(); + WriteDocuments(CachedFirestore(kBootstrapAppName)->Collection(result.path()), + docs); + return result; +} + +std::string FirestoreIntegrationTest::DocumentPath() const { + return "test-collection/" + util::CreateAutoId(); +} + +DocumentReference FirestoreIntegrationTest::Document() const { + return firestore()->Document(DocumentPath()); +} + +void FirestoreIntegrationTest::WriteDocument(DocumentReference reference, + const MapFieldValue& data) const { + Future future = reference.Set(data); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; +} + +void FirestoreIntegrationTest::WriteDocuments( + CollectionReference reference, + const std::map& data) const { + for (const auto& kv : data) { + WriteDocument(reference.Document(kv.first), kv.second); + } +} + +DocumentSnapshot FirestoreIntegrationTest::ReadDocument( + const DocumentReference& reference) const { + Future future = reference.Get(); + const DocumentSnapshot* result = Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + EXPECT_NE(nullptr, result) << DescribeFailedFuture(future) << std::endl; + return *result; +} + +QuerySnapshot FirestoreIntegrationTest::ReadDocuments( + const Query& reference) const { + Future future = reference.Get(); + const QuerySnapshot* result = Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + EXPECT_NE(nullptr, result) << DescribeFailedFuture(future) << std::endl; + return *result; +} + +void FirestoreIntegrationTest::DeleteDocument( + DocumentReference reference) const { + Future future = reference.Delete(); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; +} + +std::vector FirestoreIntegrationTest::QuerySnapshotToIds( + const QuerySnapshot& snapshot) const { + std::vector result; + for (const DocumentSnapshot& doc : snapshot.documents()) { + result.push_back(doc.id()); + } + return result; +} + +std::vector FirestoreIntegrationTest::QuerySnapshotToValues( + const QuerySnapshot& snapshot) const { + std::vector result; + for (const DocumentSnapshot& doc : snapshot.documents()) { + result.push_back(doc.GetData()); + } + return result; +} + +/* static */ +void FirestoreIntegrationTest::Await(const Future& future) { + while (future.status() == FutureStatus::kFutureStatusPending) { + if (ProcessEvents(kCheckIntervalMillis)) { + std::cout << "WARNING: app received an event requesting exit." + << std::endl; + break; + } + } +} + +void FirestoreIntegrationTest::TerminateAndRelease(Firestore* firestore) { + Await(firestore->Terminate()); + Release(firestore); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/firestore_integration_test.h b/firestore/src/tests/firestore_integration_test.h new file mode 100644 index 0000000000..e22bd38db2 --- /dev/null +++ b/firestore/src/tests/firestore_integration_test.h @@ -0,0 +1,275 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_FIRESTORE_INTEGRATION_TEST_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_FIRESTORE_INTEGRATION_TEST_H_ + +#include +#include +#include +#include +#include + +#include "app/src/assert.h" +#include "app/src/include/firebase/internal/common.h" +#include "app/src/mutex.h" +#include "firestore/src/include/firebase/firestore.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +// The interval between checks for future completion. +const int kCheckIntervalMillis = 100; + +// The timeout of waiting for a Future or a listener. +const int kTimeOutMillis = 15000; + +FirestoreInternal* CreateTestFirestoreInternal(App* app); +void InitializeFirestore(Firestore* instance); + +App* GetApp(); +App* GetApp(const char* name); +bool ProcessEvents(int msec); + +template +class EventAccumulator; + +// An EventListener class for writing tests. This listener counts the number of +// events as well as keeps track of the last result. +template +class TestEventListener : public EventListener { + public: + explicit TestEventListener(std::string name) : name_(std::move(name)) {} + + ~TestEventListener() override {} + + void OnEvent(const T& value, Error error) override { + if (print_debug_info_) { + std::cout << "TestEventListener got: "; + if (error == Error::kErrorOk) { + std::cout << &value + << " from_cache:" << value.metadata().is_from_cache() + << " has_pending_write:" + << value.metadata().has_pending_writes() << std::endl; + } else { + std::cout << "error:" << error << std::endl; + } + } + + event_count_++; + if (error != Error::kErrorOk) { + std::cerr << "ERROR: EventListener " << name_ << " got " << error + << std::endl; + if (first_error_ == Error::kErrorOk) { + first_error_ = error; + } + } + MutexLock lock(mutex_); + last_result_.push_back(value); + } + + int event_count() const { return event_count_; } + + const T& last_result(int i = 0) { + FIREBASE_ASSERT(i >= 0 && i < last_result_.size()); + MutexLock lock(mutex_); + return last_result_[last_result_.size() - 1 - i]; + } + + // Hides the STLPort-related quirk that `AddSnapshotListener` has different + // signatures depending on whether `std::function` is available. + template + ListenerRegistration AttachTo( + U* ref, MetadataChanges metadata_changes = MetadataChanges::kExclude) { +#if defined(FIREBASE_USE_STD_FUNCTION) + return ref->AddSnapshotListener( + metadata_changes, + [this](const T& result, Error error) { OnEvent(result, error); }); +#else + return ref->AddSnapshotListener(metadata_changes, this); +#endif + } + + Error first_error() { return first_error_; } + + // Set this to true to print more details for each arrived event for debug. + void set_print_debug_info(bool value) { print_debug_info_ = value; } + + private: + friend class EventAccumulator; + + std::string name_; + int event_count_ = 0; + + // We may want the last N result. So we store all in a vector in the order + // they arrived. + std::vector last_result_; + // We add a mutex to protect the calls to push_back, which is not thread-safe. + // Marked as `mutable` so that const functions can still be protected. + mutable Mutex mutex_; + + // We generally only check to see if there is any error. So we only store the + // first non-OK error, if any. + Error first_error_ = Error::kErrorOk; + + bool print_debug_info_ = false; +}; + +// Base class for Firestore integration tests. +// Note it keeps a cached of created Firestore instances, and is thread-unsafe. +class FirestoreIntegrationTest : public testing::Test { + friend class TransactionTester; + + public: + FirestoreIntegrationTest(); + FirestoreIntegrationTest(const FirestoreIntegrationTest&) = delete; + FirestoreIntegrationTest(FirestoreIntegrationTest&&) = delete; + ~FirestoreIntegrationTest() override; + + FirestoreIntegrationTest& operator=(const FirestoreIntegrationTest&) = delete; + FirestoreIntegrationTest& operator=(FirestoreIntegrationTest&&) = delete; + + protected: + App* app() { return firestore()->app(); } + + Firestore* firestore() const { return CachedFirestore(kDefaultAppName); } + + // If no Firestore instance is registered under the name, creates a new + // instance in order to have multiple Firestore clients for testing. + // Otherwise, returns the registered Firestore instance. + Firestore* CachedFirestore(const std::string& name) const; + + // Blocks until the Firestore instance corresponding to the given app name + // shuts down, deletes the instance and removes the pointer to it from the + // cache. Asserts that a Firestore instance with the given name does exist. + void DeleteFirestore(const std::string& name = kDefaultAppName); + + // Return a reference to the collection with auto-generated id. + CollectionReference Collection() const; + + // Return a reference to a collection with the path constructed by appending a + // unique id to the given name. + CollectionReference Collection(const std::string& name_prefix) const; + + // Return a reference to the collection with given content. + CollectionReference Collection( + const std::map& docs) const; + + // Return an auto-generated document path under collection "test-collection". + std::string DocumentPath() const; + + // Return a reference to the document with auto-generated id. + DocumentReference Document() const; + + // Write to the specified document and wait for the write to complete. + void WriteDocument(DocumentReference reference, + const MapFieldValue& data) const; + + // Write to the specified documents to a collection and wait for completion. + void WriteDocuments(CollectionReference reference, + const std::map& data) const; + + // Update the specified document and wait for the update to complete. + template + void UpdateDocument(DocumentReference reference, const MapType& data) const { + Future future = reference.Update(data); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + } + + // Read the specified document. + DocumentSnapshot ReadDocument(const DocumentReference& reference) const; + + // Read documents in the specified collection / query. + QuerySnapshot ReadDocuments(const Query& reference) const; + + // Delete the specified document. + void DeleteDocument(DocumentReference reference) const; + + // Convert a QuerySnapshot to the id of each document. + std::vector QuerySnapshotToIds( + const QuerySnapshot& snapshot) const; + + // Convert a QuerySnapshot to the contents of each document. + std::vector QuerySnapshotToValues( + const QuerySnapshot& snapshot) const; + + // TODO(zxu): add a helper function to block on signal. + + // A helper function to block until the future completes. + template + static const T* Await(const Future& future) { + // Instead of getting a clock, we count the cycles instead. + int cycles = kTimeOutMillis / kCheckIntervalMillis; + while (future.status() == FutureStatus::kFutureStatusPending && + cycles > 0) { + if (ProcessEvents(kCheckIntervalMillis)) { + std::cout << "WARNING: app receives an event requesting exit." + << std::endl; + return nullptr; + } + --cycles; + } + EXPECT_GT(cycles, 0) << "Waiting future timed out."; + if (future.status() == FutureStatus::kFutureStatusComplete) { + if (future.result() == nullptr) { + std::cout << "WARNING: Future failed. Error code " << future.error() + << ", message " << future.error_message() << std::endl; + } + } else { + std::cout << "WARNING: Future is not completed." << std::endl; + } + return future.result(); + } + + static void Await(const Future& future); + + // A helper function to block until there is at least n event. + template + static void Await(const TestEventListener& listener, int n = 1) { + // Instead of getting a clock, we count the cycles instead. + int cycles = kTimeOutMillis / kCheckIntervalMillis; + while (listener.event_count() < n && cycles > 0) { + if (ProcessEvents(kCheckIntervalMillis)) { + std::cout << "WARNING: app receives an event requesting exit." + << std::endl; + return; + } + --cycles; + } + EXPECT_GT(cycles, 0) << "Waiting listener timed out."; + } + + template + static std::string DescribeFailedFuture(const Future& future) { + return "WARNING: Future failed. Error code " + + std::to_string(future.error()) + ", message " + + future.error_message(); + } + + // Creates a new Firestore instance, without any caching, using a uniquely- + // generated app_name. + Firestore* CreateFirestore() const; + // Creates a new Firestore instance, without any caching, using the given + // app_name. + Firestore* CreateFirestore(const std::string& app_name) const; + + void DisableNetwork() { Await(firestore()->DisableNetwork()); } + + void EnableNetwork() { Await(firestore()->EnableNetwork()); } + + private: + template + friend class EventAccumulator; + + // Blocks until the given Firestore instance terminates, deletes the instance + // and removes the pointer to it from the cache. + void TerminateAndRelease(Firestore* firestore); + + // The Firestore instance cache. + mutable std::map firestores_; +}; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_FIRESTORE_INTEGRATION_TEST_H_ diff --git a/firestore/src/tests/firestore_test.cc b/firestore/src/tests/firestore_test.cc new file mode 100644 index 0000000000..8a14257761 --- /dev/null +++ b/firestore/src/tests/firestore_test.cc @@ -0,0 +1,1334 @@ +#if !defined(__ANDROID__) +#include // NOLINT(build/c++11) +#endif + +#if !defined(FIRESTORE_STUB_BUILD) +#include "app/src/semaphore.h" +#endif +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/util_android.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/FirestoreTest.java +// Some test cases are named differently between iOS and Android. Here we choose +// the most descriptive names. + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, GetInstance) { + // Create App. + App* app = this->app(); + EXPECT_NE(nullptr, app); + + // Get an instance. + InitResult result; + Firestore* instance = Firestore::GetInstance(app, &result); + EXPECT_EQ(kInitResultSuccess, result); + EXPECT_NE(nullptr, instance); + EXPECT_EQ(app, instance->app()); +} + +// Sanity test for stubs. +TEST_F(FirestoreIntegrationTest, TestCanCreateCollectionAndDocumentReferences) { + ASSERT_NO_THROW({ + Firestore* db = firestore(); + CollectionReference c = db->Collection("a/b/c").Document("d").Parent(); + DocumentReference d = db->Document("a/b").Collection("c/d/e").Parent(); + + CollectionReference(c).Document(); + DocumentReference(d).Parent(); + + CollectionReference(std::move(c)).Document(); + DocumentReference(std::move(d)).Parent(); + }); +} + +#if defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestStubsReturnFailedFutures) { + Firestore* db = firestore(); + Future future = db->EnableNetwork(); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorFailedPrecondition, future.error()); + + future = db->Document("foo/bar").Set( + MapFieldValue{{"foo", FieldValue::String("bar")}}); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorFailedPrecondition, future.error()); +} + +#else // defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestCanUpdateAnExistingDocument) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Update( + MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner.email", FieldValue::String("new@xyz.com")}})); + DocumentSnapshot doc = ReadDocument(document); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("new@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanUpdateAnUnknownDocument) { + DocumentReference writer_reference = + CachedFirestore("writer")->Collection("collection").Document(); + DocumentReference reader_reference = CachedFirestore("reader") + ->Collection("collection") + .Document(writer_reference.id()); + Await(writer_reference.Set(MapFieldValue{{"a", FieldValue::String("a")}})); + Await(reader_reference.Update(MapFieldValue{{"b", FieldValue::String("b")}})); + + DocumentSnapshot writer_snapshot = + *Await(writer_reference.Get(Source::kCache)); + EXPECT_TRUE(writer_snapshot.exists()); + EXPECT_THAT( + writer_snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::String("a")}})); + EXPECT_TRUE(writer_snapshot.metadata().is_from_cache()); + + Future future = reader_reference.Get(Source::kCache); + Await(future); + EXPECT_EQ(Error::kErrorUnavailable, future.error()); + + writer_snapshot = ReadDocument(writer_reference); + EXPECT_THAT(writer_snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("a")}, + {"b", FieldValue::String("b")}})); + EXPECT_FALSE(writer_snapshot.metadata().is_from_cache()); + DocumentSnapshot reader_snapshot = ReadDocument(reader_reference); + EXPECT_THAT(reader_snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("a")}, + {"b", FieldValue::String("b")}})); + EXPECT_FALSE(reader_snapshot.metadata().is_from_cache()); +} + +TEST_F(FirestoreIntegrationTest, TestCanOverwriteAnExistingDocumentUsingSet) { + DocumentReference document = Collection("rooms").Document(); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set(MapFieldValue{ + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}})}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}})}})); +} + +TEST_F(FirestoreIntegrationTest, + TestCanMergeDataWithAnExistingDocumentUsingSet) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{ + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}})}}, + SetOptions::Merge())); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanMergeServerTimestamps) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{{"untouched", FieldValue::Boolean(true)}})); + Await(document.Set( + MapFieldValue{{"time", FieldValue::ServerTimestamp()}, + {"nested", FieldValue::Map( + {{"time", FieldValue::ServerTimestamp()}})}}, + SetOptions::Merge())); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.Get("untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("time").is_timestamp()); + EXPECT_TRUE(snapshot.Get("nested.time").is_timestamp()); +} + +TEST_F(FirestoreIntegrationTest, TestCanMergeEmptyObject) { + DocumentReference document = Document(); + EventAccumulator accumulator; + ListenerRegistration registration = + accumulator.listener()->AttachTo(&document); + accumulator.Await(); + + document.Set(MapFieldValue{}); + DocumentSnapshot snapshot = accumulator.Await(); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{})); + + Await(document.Set(MapFieldValue{{"a", FieldValue::Map({})}}, + SetOptions::MergeFields({"a"}))); + snapshot = accumulator.Await(); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Map({})}})); + + Await(document.Set(MapFieldValue{{"b", FieldValue::Map({})}}, + SetOptions::Merge())); + snapshot = accumulator.Await(); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Map({})}, + {"b", FieldValue::Map({})}})); + + snapshot = *Await(document.Get(Source::kServer)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Map({})}, + {"b", FieldValue::Map({})}})); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestCanDeleteFieldUsingMerge) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.Get("untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("nested.untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("foo").is_valid()); + EXPECT_TRUE(snapshot.Get("nested.foo").is_valid()); + + Await(document.Set( + MapFieldValue{{"foo", FieldValue::Delete()}, + {"nested", FieldValue::Map(MapFieldValue{ + {"foo", FieldValue::Delete()}})}}, + SetOptions::Merge())); + snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.Get("untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("nested.untouched").boolean_value()); + EXPECT_FALSE(snapshot.Get("foo").is_valid()); + EXPECT_FALSE(snapshot.Get("nested.foo").is_valid()); +} + +TEST_F(FirestoreIntegrationTest, TestCanDeleteFieldUsingMergeFields) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}, + {"inner", FieldValue::Map({{"removed", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}})); + Await(document.Set( + MapFieldValue{ + {"foo", FieldValue::Delete()}, + {"inner", FieldValue::Map({{"foo", FieldValue::Delete()}})}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Delete()}, + {"foo", FieldValue::Delete()}})}}, + SetOptions::MergeFields({"foo", "inner", "nested.foo"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"inner", FieldValue::Map({})}, + {"nested", + FieldValue::Map({{"untouched", FieldValue::Boolean(true)}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanSetServerTimestampsUsingMergeFields) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}})); + Await(document.Set( + MapFieldValue{ + {"foo", FieldValue::ServerTimestamp()}, + {"inner", FieldValue::Map({{"foo", FieldValue::ServerTimestamp()}})}, + {"nested", + FieldValue::Map({{"foo", FieldValue::ServerTimestamp()}})}}, + SetOptions::MergeFields({"foo", "inner", "nested.foo"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.exists()); + EXPECT_TRUE(snapshot.Get("foo").is_timestamp()); + EXPECT_TRUE(snapshot.Get("inner.foo").is_timestamp()); + EXPECT_TRUE(snapshot.Get("nested.foo").is_timestamp()); +} + +TEST_F(FirestoreIntegrationTest, TestMergeReplacesArrays) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"data", FieldValue::String("old")}, + {"topLevel", FieldValue::Array( + {FieldValue::String("old"), FieldValue::String("old")})}, + {"mapInArray", FieldValue::Array({FieldValue::Map( + {{"data", FieldValue::String("old")}})})}})); + Await(document.Set( + MapFieldValue{ + {"data", FieldValue::String("new")}, + {"topLevel", FieldValue::Array({FieldValue::String("new")})}, + {"mapInArray", FieldValue::Array({FieldValue::Map( + {{"data", FieldValue::String("new")}})})}}, + SetOptions::Merge())); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"data", FieldValue::String("new")}, + {"topLevel", FieldValue::Array({FieldValue::String("new")})}, + {"mapInArray", FieldValue::Array({FieldValue::Map( + {{"data", FieldValue::String("new")}})})}})); +} + +TEST_F(FirestoreIntegrationTest, + TestCanDeepMergeDataWithAnExistingDocumentUsingSet) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("old@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("new@xyz.com")}})}}, + SetOptions::MergeFieldPaths({{"desc"}, {"owner.data", "name"}}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("old@xyz.com")}})}})); +} + +#if defined(__ANDROID__) +// TODO(b/136012313): iOS currently doesn't rethrow native exceptions as C++ +// exceptions. +TEST_F(FirestoreIntegrationTest, TestFieldMaskCannotContainMissingFields) { + DocumentReference document = Collection("rooms").Document(); + try { + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}}, + SetOptions::MergeFields({"desc", "owner"})); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Field 'owner' is specified in your field mask but not in your input " + "data.", + exception.what()); + } +} +#endif + +TEST_F(FirestoreIntegrationTest, TestFieldsNotInFieldMaskAreIgnored) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await( + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner", FieldValue::String("Sebastian")}}, + SetOptions::MergeFields({"desc"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestFieldDeletesNotInFieldMaskAreIgnored) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await( + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner", FieldValue::Delete()}}, + SetOptions::MergeFields({"desc"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestFieldTransformsNotInFieldMaskAreIgnored) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await( + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner", FieldValue::ServerTimestamp()}}, + SetOptions::MergeFields({"desc"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanSetEmptyFieldMask) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{{"desc", FieldValue::String("NewDescription")}}, + SetOptions::MergeFields({}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanSpecifyFieldsMultipleTimesInFieldMask) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("new@new.com")}})}}, + SetOptions::MergeFields({"owner.name", "owner", "owner"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("new@new.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanDeleteAFieldWithAnUpdate) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Update(MapFieldValue{{"owner.email", FieldValue::Delete()}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanUpdateFieldsWithDots) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{{"a.b", FieldValue::String("old")}, + {"c.d", FieldValue::String("old")}, + {"e.f", FieldValue::String("old")}})); + Await(document.Update({{FieldPath{"a.b"}, FieldValue::String("new")}})); + Await(document.Update({{FieldPath{"c.d"}, FieldValue::String("new")}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a.b", FieldValue::String("new")}, + {"c.d", FieldValue::String("new")}, + {"e.f", FieldValue::String("old")}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanUpdateNestedFields) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("old")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("old")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); + Await(document.Update({{"a.b", FieldValue::String("new")}})); + Await(document.Update({{"c.d", FieldValue::String("new")}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("new")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("new")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestDeleteDocument) { + DocumentReference document = Collection("rooms").Document("eros"); + WriteDocument(document, MapFieldValue{{"value", FieldValue::String("bar")}}); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"value", FieldValue::String("bar")}})); + + Await(document.Delete()); + snapshot = ReadDocument(document); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(FirestoreIntegrationTest, TestCannotUpdateNonexistentDocument) { + DocumentReference document = Collection("rooms").Document(); + Future future = + document.Update(MapFieldValue{{"owner", FieldValue::String("abc")}}); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(FirestoreIntegrationTest, TestCanRetrieveNonexistentDocument) { + DocumentReference document = Collection("rooms").Document(); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_FALSE(snapshot.exists()); + + TestEventListener listener{"for document"}; + ListenerRegistration registration = listener.AttachTo(&document); + Await(listener); + EXPECT_EQ(Error::kErrorOk, listener.first_error()); + EXPECT_FALSE(listener.last_result().exists()); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, + TestAddingToACollectionYieldsTheCorrectDocumentReference) { + DocumentReference document = Collection("rooms").Document(); + Await(document.Set(MapFieldValue{{"foo", FieldValue::Double(1.0)}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::Double(1.0)}})); +} + +TEST_F(FirestoreIntegrationTest, + TestSnapshotsInSyncListenerFiresAfterListenersInSync) { + DocumentReference document = Collection("rooms").Document(); + Await(document.Set(MapFieldValue{{"foo", FieldValue::Double(1.0)}})); + std::vector events; + + class SnapshotTestEventListener : public TestEventListener { + public: + SnapshotTestEventListener(std::string name, + std::vector* events) + : TestEventListener(std::move(name)), events_(events) {} + + void OnEvent(const DocumentSnapshot& value, Error error) override { + TestEventListener::OnEvent(value, error); + events_->push_back("doc"); + } + + private: + std::vector* events_; + }; + SnapshotTestEventListener listener{"doc", &events}; + ListenerRegistration doc_registration = listener.AttachTo(&document); + // Wait for the initial event from the backend so that we know we'll get + // exactly one snapshot event for our local write below. + Await(listener); + EXPECT_EQ(1, events.size()); + events.clear(); + +#if defined(__APPLE__) + // TODO(varconst): the implementation of `Semaphore::Post()` on Apple + // platforms has a data race which may result in semaphore data being accessed + // on the listener thread after it was destroyed on the main thread. To work + // around this, use `std::promise`. + std::promise promise; +#else + Semaphore completed{0}; +#endif + +#if defined(FIREBASE_USE_STD_FUNCTION) + ListenerRegistration sync_registration = + firestore()->AddSnapshotsInSyncListener([&] { + events.push_back("snapshots-in-sync"); + if (events.size() == 3) { +#if defined(__APPLE__) + promise.set_value(); +#else + completed.Post(); +#endif + } + }); + +#else + class SyncEventListener : public EventListener { + public: + explicit SyncEventListener(std::vector* events, + Semaphore* completed) + : events_(events), completed_(completed) {} + + void OnEvent(Error) override { + events_->push_back("snapshots-in-sync"); + if (events.size() == 3) { + completed_->Post(); + } + } + + private: + std::vector* events_ = nullptr; + Semaphore* completed_ = nullptr; + }; + SyncEventListener sync_listener{&events, &completed}; + ListenerRegistration sync_registration = + firestore()->AddSnapshotsInSyncListener(sync_listener); +#endif // defined(FIREBASE_USE_STD_FUNCTION) + + Await(document.Set(MapFieldValue{{"foo", FieldValue::Double(3.0)}})); + // Wait for the snapshots-in-sync listener to fire afterwards. +#if defined(__APPLE__) + promise.get_future().wait(); +#else + completed.Wait(); +#endif + + // We should have an initial snapshots-in-sync event, then a snapshot event + // for set(), then another event to indicate we're in sync again. + EXPECT_EQ(events, std::vector( + {"snapshots-in-sync", "doc", "snapshots-in-sync"})); + doc_registration.Remove(); + sync_registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesAreValidatedOnClient) { + // NOTE: Failure cases are validated in ValidationTest. + CollectionReference collection = Collection(); + Query query = + collection.WhereGreaterThanOrEqualTo("x", FieldValue::Integer(32)); + // Same inequality field works; + query.WhereLessThanOrEqualTo("x", FieldValue::String("cat")); + // Equality on different field works; + query.WhereEqualTo("y", FieldValue::String("cat")); + // Array contains on different field works; + query.WhereArrayContains("y", FieldValue::String("cat")); + + // Ordering by inequality field succeeds. + query.OrderBy("x"); + collection.OrderBy("x").WhereGreaterThanOrEqualTo("x", + FieldValue::Integer(32)); + + // inequality same as first order by works + query.OrderBy("x").OrderBy("y"); + collection.OrderBy("x").OrderBy("y").WhereGreaterThanOrEqualTo( + "x", FieldValue::Integer(32)); + collection.OrderBy("x", Query::Direction::kDescending) + .WhereEqualTo("y", FieldValue::String("true")); + + // Equality different than orderBy works + collection.OrderBy("x").WhereEqualTo("y", FieldValue::String("cat")); + // Array contains different than orderBy works + collection.OrderBy("x").WhereArrayContains("y", FieldValue::String("cat")); +} + +// The test harness will generate Java JUnit test regardless whether this is +// inside a #if or not. So we move #if inside instead of enclose the whole case. +TEST_F(FirestoreIntegrationTest, TestListenCanBeCalledMultipleTimes) { + // Note: this test is flaky -- the test case may finish, triggering the + // destruction of Firestore, before the async callback finishes. +#if defined(FIREBASE_USE_STD_FUNCTION) + DocumentReference document = Collection("collection").Document(); + WriteDocument(document, MapFieldValue{{"foo", FieldValue::String("bar")}}); +#if defined(__APPLE__) + // TODO(varconst): the implementation of `Semaphore::Post()` on Apple + // platforms has a data race which may result in semaphore data being accessed + // on the listener thread after it was destroyed on the main thread. To work + // around this, use `std::promise`. + std::promise promise; +#else + Semaphore completed{0}; +#endif + DocumentSnapshot resulting_data; + document.AddSnapshotListener( + [&](const DocumentSnapshot& snapshot, Error error) { + EXPECT_EQ(Error::kErrorOk, error); + document.AddSnapshotListener( + [&](const DocumentSnapshot& snapshot, Error error) { + EXPECT_EQ(Error::kErrorOk, error); + resulting_data = snapshot; +#if defined(__APPLE__) + promise.set_value(); +#else + completed.Post(); +#endif + }); + }); +#if defined(__APPLE__) + promise.get_future().wait(); +#else + completed.Wait(); +#endif + EXPECT_THAT( + resulting_data.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsNonExistent) { + DocumentReference document = Collection("rooms").Document(); + TestEventListener listener("TestNonExistent"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener); + EXPECT_EQ(1, listener.event_count()); + EXPECT_EQ(Error::kErrorOk, listener.first_error()); + EXPECT_FALSE(listener.last_result().exists()); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForAdd) { + DocumentReference document = Collection("rooms").Document(); + TestEventListener listener("TestForAdd"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener); + EXPECT_FALSE(listener.last_result().exists()); + + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(1.0)}}); + Await(listener, 3); + DocumentSnapshot snapshot = listener.last_result(1); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + snapshot = listener.last_result(); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForChange) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForChange"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener); + DocumentSnapshot snapshot = listener.last_result(); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + UpdateDocument(document, MapFieldValue{{"a", FieldValue::Double(2.0)}}); + Await(listener, 3); + snapshot = listener.last_result(1); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + snapshot = listener.last_result(); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForDelete) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForDelete"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener, 1); + DocumentSnapshot snapshot = listener.last_result(); + EXPECT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + DeleteDocument(document); + Await(listener, 2); + snapshot = listener.last_result(); + EXPECT_FALSE(snapshot.exists()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForAdd) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + TestEventListener listener("TestForCollectionAdd"); + ListenerRegistration registration = + listener.AttachTo(&collection, MetadataChanges::kInclude); + Await(listener); + EXPECT_EQ(0, listener.last_result().size()); + + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(1.0)}}); + Await(listener, 3); + QuerySnapshot snapshot = listener.last_result(1); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForChange) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForCollectionChange"); + ListenerRegistration registration = + listener.AttachTo(&collection, MetadataChanges::kInclude); + Await(listener); + QuerySnapshot snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(2.0)}}); + Await(listener, 3); + snapshot = listener.last_result(1); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForDelete) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForQueryDelete"); + ListenerRegistration registration = + listener.AttachTo(&collection, MetadataChanges::kInclude); + Await(listener); + QuerySnapshot snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + DeleteDocument(document); + Await(listener, 2); + snapshot = listener.last_result(); + EXPECT_EQ(0, snapshot.size()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, + TestMetadataOnlyChangesAreNotFiredWhenNoOptionsProvided) { + DocumentReference document = Collection().Document(); + TestEventListener listener("TestForNoMetadataOnlyChanges"); + ListenerRegistration registration = listener.AttachTo(&document); + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(1.0)}}); + Await(listener); + EXPECT_THAT( + listener.last_result().GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + WriteDocument(document, MapFieldValue{{"b", FieldValue::Double(1.0)}}); + Await(listener); + EXPECT_THAT( + listener.last_result().GetData(), + testing::ContainerEq(MapFieldValue{{"b", FieldValue::Double(1.0)}})); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentReferenceExposesFirestore) { + Firestore* db = firestore(); + // EXPECT_EQ(db, db->Document("foo/bar").firestore()); + // TODO(varconst): use the commented out check above. + // Currently, integration tests create their own Firestore instances that + // aren't registered in the main cache. Because of that, Firestore objects + // will lazily create a new Firestore instance upon the first access. This + // doesn't affect production code, only tests. + // Also, the logic in `util_ios.h` can be modified to make sure that + // `CachedFirestore` doesn't create a new Firestore instance if there isn't + // one already. + EXPECT_NE(nullptr, db->Document("foo/bar").firestore()); +} + +TEST_F(FirestoreIntegrationTest, TestCollectionReferenceExposesFirestore) { + Firestore* db = firestore(); + // EXPECT_EQ(db, db->Collection("foo").firestore()); + EXPECT_NE(nullptr, db->Collection("foo").firestore()); +} + +TEST_F(FirestoreIntegrationTest, TestQueryExposesFirestore) { + Firestore* db = firestore(); + // EXPECT_EQ(db, db->Collection("foo").Limit(5).firestore()); + EXPECT_NE(nullptr, db->Collection("foo").Limit(5).firestore()); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentReferenceEquality) { + Firestore* db = firestore(); + DocumentReference document = db->Document("foo/bar"); + EXPECT_EQ(document, db->Document("foo/bar")); + EXPECT_EQ(document, document.Collection("blah").Parent()); + + EXPECT_NE(document, db->Document("foo/BAR")); + + Firestore* another_db = CachedFirestore("another"); + EXPECT_NE(document, another_db->Document("foo/bar")); +} + +TEST_F(FirestoreIntegrationTest, TestQueryReferenceEquality) { + Firestore* db = firestore(); + Query query = db->Collection("foo").OrderBy("bar").WhereEqualTo( + "baz", FieldValue::Integer(42)); + Query query2 = db->Collection("foo").OrderBy("bar").WhereEqualTo( + "baz", FieldValue::Integer(42)); + EXPECT_EQ(query, query2); + + Query query3 = db->Collection("foo").OrderBy("BAR").WhereEqualTo( + "baz", FieldValue::Integer(42)); + EXPECT_NE(query, query3); + + // PORT_NOTE: Right now there is no way to create another Firestore in test. + // So we skip the testing of two queries with different Firestore instance. +} + +TEST_F(FirestoreIntegrationTest, TestCanTraverseCollectionsAndDocuments) { + Firestore* db = firestore(); + + // doc path from root Firestore. + EXPECT_EQ("a/b/c/d", db->Document("a/b/c/d").path()); + + // collection path from root Firestore. + EXPECT_EQ("a/b/c/d", db->Collection("a/b/c").Document("d").path()); + + // doc path from CollectionReference. + EXPECT_EQ("a/b/c/d", db->Collection("a").Document("b/c/d").path()); + + // collection path from DocumentReference. + EXPECT_EQ("a/b/c/d/e", db->Document("a/b").Collection("c/d/e").path()); +} + +TEST_F(FirestoreIntegrationTest, TestCanTraverseCollectionAndDocumentParents) { + Firestore* db = firestore(); + CollectionReference collection = db->Collection("a/b/c"); + EXPECT_EQ("a/b/c", collection.path()); + + DocumentReference doc = collection.Parent(); + EXPECT_EQ("a/b", doc.path()); + + collection = doc.Parent(); + EXPECT_EQ("a", collection.path()); + + DocumentReference invalidDoc = collection.Parent(); + EXPECT_FALSE(invalidDoc.is_valid()); +} + +TEST_F(FirestoreIntegrationTest, TestCollectionId) { + EXPECT_EQ("foo", firestore()->Collection("foo").id()); + EXPECT_EQ("baz", firestore()->Collection("foo/bar/baz").id()); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentId) { + EXPECT_EQ(firestore()->Document("foo/bar").id(), "bar"); + EXPECT_EQ(firestore()->Document("foo/bar/baz/qux").id(), "qux"); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueueWritesWhileOffline) { + // Arrange + DocumentReference document = Collection("rooms").Document("eros"); + + // Act + Await(firestore()->DisableNetwork()); + Future future = document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}}); + EXPECT_EQ(FutureStatus::kFutureStatusPending, future.status()); + Await(firestore()->EnableNetwork()); + Await(future); + + // Assert + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); +} + +TEST_F(FirestoreIntegrationTest, TestCanGetDocumentsWhileOffline) { + DocumentReference document = Collection("rooms").Document(); + Await(firestore()->DisableNetwork()); + Future future = document.Get(); + Await(future); + EXPECT_EQ(Error::kErrorUnavailable, future.error()); + + // Write the document to the local cache. + Future pending_write = document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}}); + + // The network is offline and we return a cached result. + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + EXPECT_TRUE(snapshot.metadata().is_from_cache()); + + // Enable the network and fetch the document again. + Await(firestore()->EnableNetwork()); + Await(pending_write); + snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); +} + +// Will not port the following two cases: +// TestWriteStreamReconnectsAfterIdle and +// TestWatchStreamReconnectsAfterIdle, +// both of which requires manipulating with DispatchQueue which is not exposed +// as a public API. +// Also, these tests exercise a particular part of SDK (streams), they are +// really unit tests that have to be run in integration tests setup. The +// existing Objective-C and Android tests cover these cases fairly well. + +TEST_F(FirestoreIntegrationTest, TestCanDisableAndEnableNetworking) { + // There's not currently a way to check if networking is in fact disabled, + // so for now just test that the method is well-behaved and doesn't throw. + Firestore* db = firestore(); + Await(db->EnableNetwork()); + Await(db->EnableNetwork()); + Await(db->DisableNetwork()); + Await(db->DisableNetwork()); + Await(db->EnableNetwork()); +} + +// TODO(varconst): split this test. +TEST_F(FirestoreIntegrationTest, TestToString) { + Settings settings; + settings.set_host("foo.bar"); + settings.set_ssl_enabled(false); + EXPECT_EQ( + "Settings(host='foo.bar', is_ssl_enabled=false, " + "is_persistence_enabled=true)", + settings.ToString()); + + CollectionReference collection = Collection("rooms"); + DocumentReference reference = collection.Document("eros"); + // Note: because the map is unordered, it's hard to check the case where a map + // has more than one element. + Await(reference.Set({ + {"owner", FieldValue::String("Jonny")}, + })); + EXPECT_EQ(std::string("DocumentReference(") + collection.id() + "/eros)", + reference.ToString()); + + DocumentSnapshot doc = ReadDocument(reference); + EXPECT_EQ( + "DocumentSnapshot(id=eros, " + "metadata=SnapshotMetadata{has_pending_writes=false, " + "is_from_cache=false}, doc={owner: 'Jonny'})", + doc.ToString()); +} + +// TODO(wuandy): Enable this for other platforms when they can handle +// exceptions. +#if defined(__ANDROID__) +TEST_F(FirestoreIntegrationTest, ClientCallsAfterTerminateFails) { + Await(firestore()->Terminate()); + EXPECT_THROW(Await(firestore()->DisableNetwork()), FirestoreException); +} + +TEST_F(FirestoreIntegrationTest, NewOperationThrowsAfterFirestoreTerminate) { + auto instance = firestore(); + DocumentReference reference = firestore()->Document("abc/123"); + Await(reference.Set({{"Field", FieldValue::Integer(100)}})); + + Await(instance->Terminate()); + + EXPECT_THROW(Await(reference.Get()), FirestoreException); + EXPECT_THROW(Await(reference.Update({{"Field", FieldValue::Integer(1)}})), + FirestoreException); + EXPECT_THROW(Await(reference.Set({{"Field", FieldValue::Integer(1)}})), + FirestoreException); + EXPECT_THROW(Await(instance->batch() + .Set(reference, {{"Field", FieldValue::Integer(1)}}) + .Commit()), + FirestoreException); + EXPECT_THROW(Await(instance->RunTransaction( + [reference](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + transaction.Get(reference, &error, &error_message); + return error; + })), + FirestoreException); +} + +TEST_F(FirestoreIntegrationTest, TerminateCanBeCalledMultipleTimes) { + auto instance = firestore(); + DocumentReference reference = instance->Document("abc/123"); + Await(reference.Set({{"Field", FieldValue::Integer(100)}})); + + Await(instance->Terminate()); + + EXPECT_THROW(Await(reference.Get()), FirestoreException); + + // Calling a second time should go through and change nothing. + Await(instance->Terminate()); + + EXPECT_THROW(Await(reference.Update({{"Field", FieldValue::Integer(1)}})), + FirestoreException); +} +#endif // defined(__ANDROID__) + +TEST_F(FirestoreIntegrationTest, MaintainsPersistenceAfterRestarting) { + DocumentReference doc = firestore()->Collection("col1").Document("doc1"); + auto path = doc.path(); + Await(doc.Set({{"foo", FieldValue::String("bar")}})); + DeleteFirestore(); + + DocumentReference doc_2 = firestore()->Document(path); + auto snap = Await(doc_2.Get()); + EXPECT_TRUE(snap->exists()); +} + +TEST_F(FirestoreIntegrationTest, RestartFirestoreLeadsToNewInstance) { + auto app_name = "non-default-app"; + App* app = GetApp(app_name); + Firestore* db = CreateFirestore(app->name()); + + // Shutdown `db` and create a new instance, make sure they are different + // instances. + Await(db->Terminate()); + auto db_2 = CreateFirestore(app->name()); + EXPECT_NE(db_2, db); + + // Make sure the new instance functions. + Await(db_2->Document("abc/doc").Set({{"foo", FieldValue::String("bar")}})); +} + +TEST_F(FirestoreIntegrationTest, CanStopListeningAfterTerminate) { + auto instance = firestore(); + DocumentReference reference = instance->Document("abc/123"); + EventAccumulator accumulator; + ListenerRegistration registration = + accumulator.listener()->AttachTo(&reference); + + accumulator.Await(); + Await(instance->Terminate()); + + // This should proceed without error. + registration.Remove(); + // Multiple calls should proceed as effectively a no-op. + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, WaitForPendingWritesResolves) { + DocumentReference document = Collection("abc").Document("123"); + + Await(firestore()->DisableNetwork()); + Future await_pending_writes_1 = firestore()->WaitForPendingWrites(); + Future pending_writes = + document.Set(MapFieldValue{{"desc", FieldValue::String("Description")}}); + Future await_pending_writes_2 = firestore()->WaitForPendingWrites(); + + // `await_pending_writes_1` resolves immediately because there are no pending + // writes at the time it is created. + Await(await_pending_writes_1); + EXPECT_EQ(await_pending_writes_1.status(), + FutureStatus::kFutureStatusComplete); + EXPECT_EQ(pending_writes.status(), FutureStatus::kFutureStatusPending); + EXPECT_EQ(await_pending_writes_2.status(), + FutureStatus::kFutureStatusPending); + + firestore()->EnableNetwork(); + Await(await_pending_writes_2); + EXPECT_EQ(await_pending_writes_2.status(), + FutureStatus::kFutureStatusComplete); +} + +// TODO(wuandy): This test requires to create underlying firestore instance with +// a MockCredentialProvider first. +// TEST_F(FirestoreIntegrationTest, WaitForPendingWritesFailsWhenUserChanges) {} + +TEST_F(FirestoreIntegrationTest, + WaitForPendingWritesResolvesWhenOfflineIfThereIsNoPending) { + Await(firestore()->DisableNetwork()); + Future await_pending_writes = firestore()->WaitForPendingWrites(); + + // `await_pending_writes` resolves immediately because there are no pending + // writes at the time it is created. + Await(await_pending_writes); + EXPECT_EQ(await_pending_writes.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(FirestoreIntegrationTest, CanClearPersistenceAfterRestarting) { + Firestore* db = CreateFirestore(); + App* app = db->app(); + std::string app_name = app->name(); + + DocumentReference document = db->Collection("a").Document("b"); + std::string path = document.path(); + WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}}); + + // ClearPersistence() requires Firestore to be terminated. Delete the app and + // the Firestore instance to emulate the way an end user would do this. + Await(db->Terminate()); + Await(db->ClearPersistence()); + delete db; + delete app; + + // We restart the app with the same name and options to check that the + // previous instance's persistent storage is actually cleared after the + // restart. Calling firestore() here would create a new instance of firestore, + // which defeats the purpose of this test. + Firestore* db_2 = CreateFirestore(app_name); + DocumentReference document_2 = db_2->Document(path); + Future await_get = document_2.Get(Source::kCache); + Await(await_get); + EXPECT_EQ(await_get.status(), FutureStatus::kFutureStatusComplete); + EXPECT_EQ(await_get.error(), Error::kErrorUnavailable); +} + +TEST_F(FirestoreIntegrationTest, CanClearPersistenceOnANewFirestoreInstance) { + Firestore* db = CreateFirestore(); + App* app = db->app(); + std::string app_name = app->name(); + + DocumentReference document = db->Collection("a").Document("b"); + std::string path = document.path(); + WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}}); + + Await(db->Terminate()); + delete db; + delete app; + + // We restart the app with the same name and options to check that the + // previous instance's persistent storage is actually cleared after the + // restart. Calling firestore() here would create a new instance of firestore, + // which defeats the purpose of this test. + Firestore* db_2 = CreateFirestore(app_name); + Await(db_2->ClearPersistence()); + DocumentReference document_2 = db_2->Document(path); + Future await_get = document_2.Get(Source::kCache); + Await(await_get); + EXPECT_EQ(await_get.status(), FutureStatus::kFutureStatusComplete); + EXPECT_EQ(await_get.error(), Error::kErrorUnavailable); +} + +TEST_F(FirestoreIntegrationTest, ClearPersistenceWhileRunningFails) { + // Call EnableNetwork() in order to ensure that Firestore is fully + // initialized before clearing persistence. EnableNetwork() is chosen because + // it is easy to call. + Await(firestore()->EnableNetwork()); + Future await_clear_persistence = firestore()->ClearPersistence(); + Await(await_clear_persistence); + EXPECT_EQ(await_clear_persistence.status(), + FutureStatus::kFutureStatusComplete); + EXPECT_EQ(await_clear_persistence.error(), Error::kErrorFailedPrecondition); +} + +// Note: this test only exists in C++. +TEST_F(FirestoreIntegrationTest, DomainObjectsReferToSameFirestoreInstance) { + EXPECT_EQ(firestore(), firestore()->Document("foo/bar").firestore()); + EXPECT_EQ(firestore(), firestore()->Collection("foo").firestore()); +} + +#endif // defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/includes_test.cc b/firestore/src/tests/includes_test.cc new file mode 100644 index 0000000000..01b64bde13 --- /dev/null +++ b/firestore/src/tests/includes_test.cc @@ -0,0 +1,87 @@ +#include + +#include "devtools/build/runtime/get_runfiles_dir.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +// This class is a friend of `Firestore`, necessary to access `GetTestInstance`. +class IncludesTest : public testing::Test { + public: + Firestore* CreateFirestore(App* app) { + return new Firestore(CreateTestFirestoreInternal(app)); + } +}; + +namespace { + +struct TestListener : EventListener { + void OnEvent(const int&, Error) override {} +}; + +struct TestTransactionFunction : TransactionFunction { + Error Apply(Transaction&, std::string&) override { return Error::kErrorOk; } +}; + +// This test makes sure that all the objects in Firestore public API are +// available from just including "firestore.h". +// If this test compiles, that is sufficient. +// Not using `FirestoreIntegrationTest` to avoid any headers it includes. +TEST_F(IncludesTest, TestIncludingFirestoreHeaderIsSufficient) { + std::string google_json_dir = devtools_build::testonly::GetTestSrcdir() + + "/google3/firebase/firestore/client/cpp/"; + App::SetDefaultConfigPath(google_json_dir.c_str()); + +#if defined(__ANDROID__) + App* app = App::Create(nullptr, nullptr); + +#elif defined(FIRESTORE_STUB_BUILD) + // Stubs don't load values from `GoogleService-Info.plist`/etc., so the app + // has to be configured explicitly. + AppOptions options; + options.set_project_id("foo"); + options.set_app_id("foo"); + options.set_api_key("foo"); + App* app = App::Create(options); + +#else + App* app = App::Create(); + +#endif // defined(__ANDROID__) + + Firestore* firestore = CreateFirestore(app); + + // Check that Firestore isn't just forward-declared. + DocumentReference doc = firestore->Document("foo/bar"); + Future future = doc.Get(); + DocumentChange doc_change; + DocumentReference doc_ref; + DocumentSnapshot doc_snap; + FieldPath field_path; + FieldValue field_value; + ListenerRegistration listener_registration; + MapFieldValue map_field_value; + MetadataChanges metadata_changes = MetadataChanges::kExclude; + Query query; + QuerySnapshot query_snapshot; + SetOptions set_options; + Settings settings; + SnapshotMetadata snapshot_metadata; + Source source = Source::kDefault; + // Cannot default-construct a `Transaction`. + WriteBatch write_batch; + + TestListener test_listener; + TestTransactionFunction test_transaction_function; + + Timestamp timestamp; + GeoPoint geo_point; + Error error = Error::kErrorOk; +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/listener_registration_test.cc b/firestore/src/tests/listener_registration_test.cc new file mode 100644 index 0000000000..44568d918f --- /dev/null +++ b/firestore/src/tests/listener_registration_test.cc @@ -0,0 +1,185 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#if defined(__ANDROID__) +#include "firestore/src/android/listener_registration_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/listener_registration_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ListenerRegistrationTest.java + +namespace firebase { +namespace firestore { + +using ListenerRegistrationCommonTest = testing::Test; + +class ListenerRegistrationTest : public FirestoreIntegrationTest { + public: + ListenerRegistrationTest() { + firestore()->set_log_level(LogLevel::kLogLevelDebug); + } +}; + +// These tests don't work with stubs. +#if !defined(FIRESTORE_STUB_BUILD) + +TEST_F(ListenerRegistrationTest, TestCanBeRemoved) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + + TestEventListener listener_one("a listener to be removed"); + TestEventListener listener_two("a listener to be removed"); + ListenerRegistration one = listener_one.AttachTo(&collection); + ListenerRegistration two = listener_two.AttachTo(&document); + + // Initial events + Await(listener_one); + Await(listener_two); + EXPECT_EQ(1, listener_one.event_count()); + EXPECT_EQ(1, listener_two.event_count()); + + // Trigger new events + WriteDocument(document, {{"foo", FieldValue::String("bar")}}); + + // Write events should have triggered + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(2, listener_two.event_count()); + + // No more events should occur + one.Remove(); + two.Remove(); + + WriteDocument(document, {{"foo", FieldValue::String("new-bar")}}); + + // Assert no events actually occurred + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(2, listener_two.event_count()); +} + +TEST_F(ListenerRegistrationTest, TestCanBeRemovedTwice) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + + TestEventListener listener_one("a listener to be removed"); + TestEventListener listener_two("a listener to be removed"); + ListenerRegistration one = listener_one.AttachTo(&collection); + ListenerRegistration two = listener_two.AttachTo(&document); + + one.Remove(); + EXPECT_NO_THROW(one.Remove()); + + two.Remove(); + EXPECT_NO_THROW(two.Remove()); +} + +TEST_F(ListenerRegistrationTest, TestCanBeRemovedIndependently) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + + TestEventListener listener_one("listener one"); + TestEventListener listener_two("listener two"); + ListenerRegistration one = listener_one.AttachTo(&collection); + ListenerRegistration two = listener_two.AttachTo(&collection); + + // Initial events + Await(listener_one); + Await(listener_two); + + // Triger new events + WriteDocument(document, {{"foo", FieldValue::String("bar")}}); + + // Write events should have triggered + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(2, listener_two.event_count()); + + // Should leave listener number two unaffected + one.Remove(); + + WriteDocument(document, {{"foo", FieldValue::String("new-bar")}}); + + // Assert only events for listener number two actually occurred + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(3, listener_two.event_count()); + + // No more events should occur + two.Remove(); + + // The following check does not exist in the corresponding Android and iOS + // native client SDKs tests. + WriteDocument(document, {{"foo", FieldValue::String("brand-new-bar")}}); + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(3, listener_two.event_count()); +} + +#endif // defined(FIRESTORE_STUB_BUILD) + +#if defined(__ANDROID__) +// TODO(b/136011600): the mechanism for creating internals doesn't work on iOS. +// The most valuable test is making sure that a copy of a registration can be +// used to remove the listener. + +TEST_F(ListenerRegistrationCommonTest, Construction) { + ListenerRegistrationInternal* internal = + testutil::NewInternal(); + ListenerRegistration registration = FirestoreInternal::Wrap(internal); + EXPECT_EQ(internal, FirestoreInternal::Internal( + registration)); + + ListenerRegistration reg_default; + EXPECT_EQ(nullptr, FirestoreInternal::Internal( + reg_default)); + + ListenerRegistration reg_copy(registration); + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_copy)); + + ListenerRegistration reg_move(std::move(registration)); + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_move)); + + delete internal; +} + +TEST_F(ListenerRegistrationCommonTest, Assignment) { + ListenerRegistrationInternal* internal = + testutil::NewInternal(); + ListenerRegistration registration = FirestoreInternal::Wrap(internal); + ListenerRegistration reg_copy; + reg_copy = registration; + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_copy)); + + ListenerRegistration reg_move; + reg_move = std::move(registration); + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_move)); + + delete internal; +} + +TEST_F(ListenerRegistrationCommonTest, Remove) { + ListenerRegistrationInternal* internal = + testutil::NewInternal(); + ListenerRegistration registration = FirestoreInternal::Wrap(internal); + ListenerRegistration reg_copy; + reg_copy = registration; + + registration.Remove(); + reg_copy.Remove(); + + delete internal; +} + +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/numeric_transforms_test.cc b/firestore/src/tests/numeric_transforms_test.cc new file mode 100644 index 0000000000..79f8609a0f --- /dev/null +++ b/firestore/src/tests/numeric_transforms_test.cc @@ -0,0 +1,204 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior; + +class NumericTransformsTest : public FirestoreIntegrationTest { + public: + NumericTransformsTest() { + doc_ref_ = Document(); + listener_ = + accumulator_.listener()->AttachTo(&doc_ref_, MetadataChanges::kInclude); + + // Wait for initial null snapshot to avoid potential races. + DocumentSnapshot initial_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_FALSE(initial_snapshot.exists()); + } + + ~NumericTransformsTest() override { listener_.Remove(); } + + protected: + /** Writes values and waits for the corresponding snapshot. */ + void WriteInitialData(const MapFieldValue& doc) { + WriteDocument(doc_ref_, doc); + + accumulator_.AwaitRemoteEvent(); + } + + void ExpectLocalAndRemoteValue(int value) { + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(value, snap.Get("sum").integer_value()); + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(value, snap.Get("sum").integer_value()); + } + + void ExpectLocalAndRemoteValue(double value) { + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(value, snap.Get("sum").double_value()); + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(value, snap.Get("sum").double_value()); + } + + // A document reference to read and write. + DocumentReference doc_ref_; + + // Accumulator used to capture events during the test. + EventAccumulator accumulator_; + + // Listener registration for a listener maintained during the course of the + // test. + ListenerRegistration listener_; +}; + +TEST_F(NumericTransformsTest, CreateDocumentWithIncrement) { + Await(doc_ref_.Set({{"sum", FieldValue::Increment(1337)}})); + + ExpectLocalAndRemoteValue(1337); +} + +TEST_F(NumericTransformsTest, MergeOnNonExistingDocumentWithIncrement) { + MapFieldValue data = {{"sum", FieldValue::Integer(1337)}}; + + Await(doc_ref_.Set(data, SetOptions::Merge())); + + ExpectLocalAndRemoteValue(1337); +} + +TEST_F(NumericTransformsTest, IntegerIncrementWithExistingInteger) { + WriteInitialData({{"sum", FieldValue::Integer(1337)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1)}})); + + ExpectLocalAndRemoteValue(1338); +} + +TEST_F(NumericTransformsTest, DoubleIncrementWithExistingDouble) { + WriteInitialData({{"sum", FieldValue::Double(13.37)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}})); + + ExpectLocalAndRemoteValue(13.47); +} + +TEST_F(NumericTransformsTest, IntegerIncrementWithExistingDouble) { + WriteInitialData({{"sum", FieldValue::Double(13.37)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1)}})); + + ExpectLocalAndRemoteValue(14.37); +} + +TEST_F(NumericTransformsTest, DoubleIncrementWithExistingInteger) { + WriteInitialData({{"sum", FieldValue::Integer(1337)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}})); + + ExpectLocalAndRemoteValue(1337.1); +} + +TEST_F(NumericTransformsTest, IntegerIncrementWithExistingString) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1337)}})); + + ExpectLocalAndRemoteValue(1337); +} + +TEST_F(NumericTransformsTest, DoubleIncrementWithExistingString) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(13.37)}})); + + ExpectLocalAndRemoteValue(13.37); +} + +TEST_F(NumericTransformsTest, MultipleDoubleIncrements) { + WriteInitialData({{"sum", FieldValue::Double(0.0)}}); + + DisableNetwork(); + + doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(0.01)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(0.001)}}); + + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(0.1, snap.Get("sum").double_value()); + + snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(0.11, snap.Get("sum").double_value()); + + snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(0.111, snap.Get("sum").double_value()); + + EnableNetwork(); + + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_DOUBLE_EQ(0.111, snap.Get("sum").double_value()); +} + +TEST_F(NumericTransformsTest, IncrementTwiceInABatch) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + WriteBatch batch = firestore()->batch(); + + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(1)}}); + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(1)}}); + + Await(batch.Commit()); + + ExpectLocalAndRemoteValue(2); +} + +TEST_F(NumericTransformsTest, IncrementDeleteIncrementInABatch) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + WriteBatch batch = firestore()->batch(); + + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(1)}}); + batch.Update(doc_ref_, {{"sum", FieldValue::Delete()}}); + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(3)}}); + + Await(batch.Commit()); + + ExpectLocalAndRemoteValue(3); +} + +TEST_F(NumericTransformsTest, ServerTimestampAndIncrement) { + DisableNetwork(); + + doc_ref_.Set({{"sum", FieldValue::ServerTimestamp()}}); + doc_ref_.Set({{"sum", FieldValue::Increment(1)}}); + + DocumentSnapshot snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + snapshot.Get("sum", ServerTimestampBehavior::kEstimate).is_timestamp()); + + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(1, snap.Get("sum").integer_value()); + + EnableNetwork(); + + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(1, snap.Get("sum").integer_value()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/query_network_test.cc b/firestore/src/tests/query_network_test.cc new file mode 100644 index 0000000000..f8b5627ae5 --- /dev/null +++ b/firestore/src/tests/query_network_test.cc @@ -0,0 +1,148 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/query_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/query_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/QueryTest.java + +namespace firebase { +namespace firestore { + +class QueryNetworkTest : public FirestoreIntegrationTest { + protected: + void TestCanHaveMultipleMutationsWhileOfflineImpl() { + CollectionReference collection = Collection(); + + // set a few docs to known values + WriteDocuments(collection, + {{"doc1", {{"key1", FieldValue::String("value1")}}}, + {"doc2", {{"key2", FieldValue::String("value2")}}}}); + + // go offline for the rest of this test + Await(firestore()->DisableNetwork()); + + // apply *multiple* mutations while offline + collection.Document("doc1").Set({{"key1b", FieldValue::String("value1b")}}); + collection.Document("doc2").Set({{"key2b", FieldValue::String("value2b")}}); + + QuerySnapshot snapshot = ReadDocuments(collection); + EXPECT_TRUE(snapshot.metadata().is_from_cache()); + EXPECT_THAT(QuerySnapshotToValues(snapshot), + testing::ElementsAre( + MapFieldValue{{"key1b", FieldValue::String("value1b")}}, + MapFieldValue{{"key2b", FieldValue::String("value2b")}})); + + Await(firestore()->EnableNetwork()); + } + + void TestWatchSurvivesNetworkDisconnectImpl() { + CollectionReference collection = Collection(); + EventAccumulator accumulator; + accumulator.listener()->set_print_debug_info(true); + ListenerRegistration registration = accumulator.listener()->AttachTo( + &collection, MetadataChanges::kInclude); + EXPECT_TRUE(accumulator.AwaitRemoteEvent().empty()); + + Await(firestore()->DisableNetwork()); + collection.Add(MapFieldValue{{"foo", FieldValue::ServerTimestamp()}}); + Await(firestore()->EnableNetwork()); + + QuerySnapshot snapshot = accumulator.AwaitServerEvent(); + EXPECT_FALSE(snapshot.empty()); + EXPECT_EQ(1, snapshot.size()); + + registration.Remove(); + } + + void TestQueriesFireFromCacheWhenOfflineImpl() { + CollectionReference collection = + Collection({{"a", {{"foo", FieldValue::Integer(1)}}}}); + EventAccumulator accumulator; + accumulator.listener()->set_print_debug_info(true); + ListenerRegistration registration = accumulator.listener()->AttachTo( + &collection, MetadataChanges::kInclude); + + // initial event + QuerySnapshot snapshot = accumulator.AwaitServerEvent(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"foo", FieldValue::Integer(1)}})); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + // offline event with is_from_cache=true + Await(firestore()->DisableNetwork()); + snapshot = accumulator.Await(); + EXPECT_TRUE(snapshot.metadata().is_from_cache()); + + // back online event with is_from_cache=false + Await(firestore()->EnableNetwork()); + snapshot = accumulator.Await(); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + registration.Remove(); + } +}; + +#if defined(__ANDROID__) +// Due to how the integration test is set on Android, we cannot make the tests +// that call DisableNetwork/EnableNetwork run in parallel. So we manually make +// them here in a separate test file and run in serial. + +TEST_F(QueryNetworkTest, EnableDisableNetwork) { + std::cout + << "[ RUN ] " + "FirestoreIntegrationTest.TestCanHaveMultipleMutationsWhileOffline" + << std::endl; + TestCanHaveMultipleMutationsWhileOfflineImpl(); + std::cout + << "[ DONE ] " + "FirestoreIntegrationTest.TestCanHaveMultipleMutationsWhileOffline" + << std::endl; + + std::cout + << "[ RUN ] FirestoreIntegrationTest.WatchSurvivesNetworkDisconnect" + << std::endl; + TestWatchSurvivesNetworkDisconnectImpl(); + std::cout + << "[ DONE ] FirestoreIntegrationTest.WatchSurvivesNetworkDisconnect" + << std::endl; + + std::cout << "[ RUN ] " + "FirestoreIntegrationTest.TestQueriesFireFromCacheWhenOffline" + << std::endl; + TestQueriesFireFromCacheWhenOfflineImpl(); + std::cout << "[ DONE ] " + "FirestoreIntegrationTest.TestQueriesFireFromCacheWhenOffline" + << std::endl; +} + +#else + +TEST_F(QueryNetworkTest, TestCanHaveMultipleMutationsWhileOffline) { + TestCanHaveMultipleMutationsWhileOfflineImpl(); +} + +TEST_F(QueryNetworkTest, TestWatchSurvivesNetworkDisconnect) { + TestWatchSurvivesNetworkDisconnectImpl(); +} + +TEST_F(QueryNetworkTest, TestQueriesFireFromCacheWhenOffline) { + TestQueriesFireFromCacheWhenOfflineImpl(); +} + +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/query_snapshot_test.cc b/firestore/src/tests/query_snapshot_test.cc new file mode 100644 index 0000000000..f61e1117df --- /dev/null +++ b/firestore/src/tests/query_snapshot_test.cc @@ -0,0 +1,34 @@ +#include + +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/query_snapshot_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/query_snapshot_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using QuerySnapshotTest = testing::Test; + +TEST_F(QuerySnapshotTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(QuerySnapshotTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/query_test.cc b/firestore/src/tests/query_test.cc new file mode 100644 index 0000000000..a9550c84b0 --- /dev/null +++ b/firestore/src/tests/query_test.cc @@ -0,0 +1,697 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/query_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/stub/query_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/firestore_errors.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/QueryTest.java +// +// Some test cases are moved to query_network_test.cc. Check that file for more +// details. + +namespace firebase { +namespace firestore { + +using QueryTest = testing::Test; + +#if !defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestLimitQueries) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + QuerySnapshot snapshot = ReadDocuments(collection.Limit(2)); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("a")}}, + {{"k", FieldValue::String("b")}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestLimitQueriesUsingDescendingSortOrder) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Integer(0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Integer(1)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Integer(1)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Integer(2)}}}}); + QuerySnapshot snapshot = ReadDocuments(collection.Limit(2).OrderBy( + FieldPath({"sort"}), Query::Direction::kDescending)); + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("d")}, {"sort", FieldValue::Integer(2)}}, + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Integer(1)}}}), + QuerySnapshotToValues(snapshot)); +} + +#if defined(__ANDROID__) +TEST_F(FirestoreIntegrationTest, TestLimitToLastMustAlsoHaveExplicitOrderBy) { + CollectionReference collection = Collection(); + + EXPECT_THROW(Await(collection.LimitToLast(2).Get()), FirestoreException); +} +#endif // defined(__ANDROID__) + +// Two queries that mapped to the same target ID are referred to as "mirror +// queries". An example for a mirror query is a LimitToLast() query and a +// Limit() query that share the same backend Target ID. Since LimitToLast() +// queries are sent to the backend with a modified OrderBy() clause, they can +// map to the same target representation as Limit() query, even if both queries +// appear separate to the user. +TEST_F(FirestoreIntegrationTest, + TestListenUnlistenRelistenSequenceOfMirrorQueries) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Integer(0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Integer(1)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Integer(1)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Integer(2)}}}}); + + // Set up `limit` query. + Query limit = + collection.Limit(2).OrderBy("sort", Query::Direction::kAscending); + EventAccumulator limit_accumulator; + ListenerRegistration limit_registration = + limit_accumulator.listener()->AttachTo(&limit); + + // Set up mirroring `limitToLast` query. + Query limit_to_last = + collection.LimitToLast(2).OrderBy("sort", Query::Direction::kDescending); + EventAccumulator limit_to_last_accumulator; + ListenerRegistration limit_to_last_registration = + limit_to_last_accumulator.listener()->AttachTo(&limit_to_last); + + // Verify both queries get expected result. + QuerySnapshot snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}}, + MapFieldValue{{"k", FieldValue::String("b")}, + {"sort", FieldValue::Integer(1)}})); + snapshot = limit_to_last_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("b")}, + {"sort", FieldValue::Integer(1)}}, + MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}})); + + // Unlisten then re-listen to the `limit` query. + limit_registration.Remove(); + limit_registration = limit_accumulator.listener()->AttachTo(&limit); + + // Verify `limit` query still works. + snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}}, + MapFieldValue{{"k", FieldValue::String("b")}, + {"sort", FieldValue::Integer(1)}})); + + // Add a document that would change the result set. + Await(collection.Add(MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}})); + + // Verify both queries get expected result. + snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}}, + MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}})); + snapshot = limit_to_last_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}}, + MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}})); + + // Unlisten to `LimitToLast`, update a doc, then relisten to `LimitToLast` + limit_to_last_registration.Remove(); + Await(collection.Document("a").Update(MapFieldValue{ + {"k", FieldValue::String("a")}, {"sort", FieldValue::Integer(-2)}})); + limit_to_last_registration = + limit_to_last_accumulator.listener()->AttachTo(&limit_to_last); + + // Verify both queries get expected result. + snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(-2)}}, + MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}})); + snapshot = limit_to_last_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}}, + MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(-2)}})); +} + +TEST_F(FirestoreIntegrationTest, + TestKeyOrderIsDescendingForDescendingInequality) { + CollectionReference collection = + Collection({{"a", {{"foo", FieldValue::Integer(42)}}}, + {"b", {{"foo", FieldValue::Double(42.0)}}}, + {"c", {{"foo", FieldValue::Integer(42)}}}, + {"d", {{"foo", FieldValue::Integer(21)}}}, + {"e", {{"foo", FieldValue::Double(21.0)}}}, + {"f", {{"foo", FieldValue::Integer(66)}}}, + {"g", {{"foo", FieldValue::Double(66.0)}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.WhereGreaterThan("foo", FieldValue::Integer(21)) + .OrderBy(FieldPath({"foo"}), Query::Direction::kDescending)); + EXPECT_EQ(std::vector({"g", "f", "c", "b", "a"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestUnaryFilterQueries) { + CollectionReference collection = Collection( + {{"a", {{"null", FieldValue::Null()}, {"nan", FieldValue::Double(NAN)}}}, + {"b", {{"null", FieldValue::Null()}, {"nan", FieldValue::Integer(0)}}}, + {"c", + {{"null", FieldValue::Boolean(false)}, + {"nan", FieldValue::Double(NAN)}}}}); + QuerySnapshot snapshot = + ReadDocuments(collection.WhereEqualTo("null", FieldValue::Null()) + .WhereEqualTo("nan", FieldValue::Double(NAN))); + EXPECT_EQ(std::vector({{{"null", FieldValue::Null()}, + {"nan", FieldValue::Double(NAN)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestQueryWithFieldPaths) { + CollectionReference collection = + Collection({{"a", {{"a", FieldValue::Integer(1)}}}, + {"b", {{"a", FieldValue::Integer(2)}}}, + {"c", {{"a", FieldValue::Integer(3)}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.WhereLessThan(FieldPath({"a"}), FieldValue::Integer(3)) + .OrderBy(FieldPath({"a"}), Query::Direction::kDescending)); + EXPECT_EQ(std::vector({"b", "a"}), QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestFilterOnInfinity) { + CollectionReference collection = + Collection({{"a", {{"inf", FieldValue::Double(INFINITY)}}}, + {"b", {{"inf", FieldValue::Double(-INFINITY)}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.WhereEqualTo("inf", FieldValue::Double(INFINITY))); + EXPECT_EQ( + std::vector({{{"inf", FieldValue::Double(INFINITY)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestWillNotGetMetadataOnlyUpdates) { + CollectionReference collection = + Collection({{"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}}); + + TestEventListener listener("no metadata-only update"); + ListenerRegistration registration = listener.AttachTo(&collection); + Await(listener); + EXPECT_EQ(1, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + + WriteDocument(collection.Document("a"), {{"v", FieldValue::String("a1")}}); + EXPECT_EQ(2, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, + TestCanListenForTheSameQueryWithDifferentOptions) { + CollectionReference collection = Collection(); + WriteDocuments(collection, {{"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}}); + + // Add two listeners, one tracking metadata-change while the other not. + TestEventListener listener("no metadata-only update"); + TestEventListener listener_full("include metadata update"); + + ListenerRegistration registration_full = + listener_full.AttachTo(&collection, MetadataChanges::kInclude); + ListenerRegistration registration = listener.AttachTo(&collection); + + Await(listener); + Await(listener_full, 2); // Let's make sure both events triggered. + + EXPECT_EQ(1, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + EXPECT_EQ(2, listener_full.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result(1))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result())); + EXPECT_TRUE(listener_full.last_result(1).metadata().is_from_cache()); + EXPECT_FALSE(listener_full.last_result().metadata().is_from_cache()); + + // Change document to trigger the listeners. + WriteDocument(collection.Document("a"), {{"v", FieldValue::String("a1")}}); + // Only one event without options + EXPECT_EQ(2, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + // Expect two events for the write, once from latency compensation and once + // from the acknowledgement from the server. + Await(listener_full, 4); // Let's make sure both events triggered. + EXPECT_EQ(4, listener_full.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result(1))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result())); + EXPECT_TRUE(listener_full.last_result(1).metadata().has_pending_writes()); + EXPECT_FALSE(listener_full.last_result().metadata().has_pending_writes()); + + // Change document again to trigger the listeners. + WriteDocument(collection.Document("b"), {{"v", FieldValue::String("b1")}}); + // Only one event without options + EXPECT_EQ(3, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b1")}}}), + QuerySnapshotToValues(listener.last_result())); + // Expect two events for the write, once from latency compensation and once + // from the acknowledgement from the server. + Await(listener_full, 6); // Let's make sure both events triggered. + EXPECT_EQ(6, listener_full.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b1")}}}), + QuerySnapshotToValues(listener_full.last_result(1))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b1")}}}), + QuerySnapshotToValues(listener_full.last_result())); + EXPECT_TRUE(listener_full.last_result(1).metadata().has_pending_writes()); + EXPECT_FALSE(listener_full.last_result().metadata().has_pending_writes()); + + // Unregister listeners. + registration.Remove(); + registration_full.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestCanListenForQueryMetadataChanges) { + CollectionReference collection = + Collection({{"1", + {{"sort", FieldValue::Double(1.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::Integer(1)}}}, + {"2", + {{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::Integer(2)}}}, + {"3", + {{"sort", FieldValue::Double(3.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::Integer(3)}}}, + {"4", + {{"sort", FieldValue::Double(4.0)}, + {"filter", FieldValue::Boolean(false)}, + {"key", FieldValue::Integer(4)}}}}); + + // The first query does not have any document cached. + TestEventListener listener1("listener to the first query"); + Query collection_with_filter1 = + collection.WhereLessThan("key", FieldValue::Integer(4)); + ListenerRegistration registration1 = + listener1.AttachTo(&collection_with_filter1); + Await(listener1); + EXPECT_EQ(1, listener1.event_count()); + EXPECT_EQ(std::vector({"1", "2", "3"}), + QuerySnapshotToIds(listener1.last_result())); + + // The second query has document cached from the first query. + TestEventListener listener2("listener to the second query"); + Query collection_with_filter2 = + collection.WhereEqualTo("filter", FieldValue::Boolean(true)); + ListenerRegistration registration2 = + listener2.AttachTo(&collection_with_filter2, MetadataChanges::kInclude); + Await(listener2, 2); // Let's make sure both events triggered. + EXPECT_EQ(2, listener2.event_count()); + EXPECT_EQ(std::vector({"1", "2", "3"}), + QuerySnapshotToIds(listener2.last_result(1))); + EXPECT_EQ(std::vector({"1", "2", "3"}), + QuerySnapshotToIds(listener2.last_result())); + EXPECT_TRUE(listener2.last_result(1).metadata().is_from_cache()); + EXPECT_FALSE(listener2.last_result().metadata().is_from_cache()); + + // Unregister listeners. + registration1.Remove(); + registration2.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestCanExplicitlySortByDocumentId) { + CollectionReference collection = + Collection({{"a", {{"key", FieldValue::String("a")}}}, + {"b", {{"key", FieldValue::String("b")}}}, + {"c", {{"key", FieldValue::String("c")}}}}); + // Ideally this would be descending to validate it's different than + // the default, but that requires an extra index + QuerySnapshot snapshot = + ReadDocuments(collection.OrderBy(FieldPath::DocumentId())); + EXPECT_EQ(std::vector({"a", "b", "c"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueryByDocumentId) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + // Query by Document Id. + QuerySnapshot snapshot1 = ReadDocuments(collection.WhereEqualTo( + FieldPath::DocumentId(), FieldValue::String("ab"))); + EXPECT_EQ(std::vector({"ab"}), QuerySnapshotToIds(snapshot1)); + + // Query by Document Ids. + QuerySnapshot snapshot2 = ReadDocuments( + collection + .WhereGreaterThan(FieldPath::DocumentId(), FieldValue::String("aa")) + .WhereLessThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("ba"))); + EXPECT_EQ(std::vector({"ab", "ba"}), + QuerySnapshotToIds(snapshot2)); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueryByDocumentIdUsingRefs) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + // Query by Document Id. + QuerySnapshot snapshot1 = ReadDocuments(collection.WhereEqualTo( + FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("ab")))); + EXPECT_EQ(std::vector({"ab"}), QuerySnapshotToIds(snapshot1)); + + // Query by Document Ids. + QuerySnapshot snapshot2 = ReadDocuments( + collection + .WhereGreaterThan(FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("aa"))) + .WhereLessThanOrEqualTo( + FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("ba")))); + EXPECT_EQ(std::vector({"ab", "ba"}), + QuerySnapshotToIds(snapshot2)); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueryWithAndWithoutDocumentKey) { + CollectionReference collection = Collection(); + collection.Add({}); + QuerySnapshot snapshot1 = ReadDocuments(collection.OrderBy( + FieldPath::DocumentId(), Query::Direction::kAscending)); + QuerySnapshot snapshot2 = ReadDocuments(collection); + + EXPECT_EQ(QuerySnapshotToValues(snapshot1), QuerySnapshotToValues(snapshot2)); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseArrayContainsFilters) { + CollectionReference collection = Collection( + {{"a", {{"array", FieldValue::Array({FieldValue::Integer(42)})}}}, + {"b", + {{"array", + FieldValue::Array({FieldValue::String("a"), FieldValue::Integer(42), + FieldValue::String("c")})}}}, + {"c", + {{"array", + FieldValue::Array( + {FieldValue::Double(41.999), FieldValue::String("42"), + FieldValue::Map( + {{"a", FieldValue::Array({FieldValue::Integer(42)})}})})}}}, + {"d", + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}}); + // Search for 42 + QuerySnapshot snapshot = ReadDocuments( + collection.WhereArrayContains("array", FieldValue::Integer(42))); + EXPECT_EQ( + std::vector( + {{{"array", FieldValue::Array({FieldValue::Integer(42)})}}, + {{"array", FieldValue::Array({FieldValue::String("a"), + FieldValue::Integer(42), + FieldValue::String("c")})}}, + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}), + QuerySnapshotToValues(snapshot)); + + // NOTE: The backend doesn't currently support null, NaN, objects, or arrays, + // so there isn't much of anything else interesting to test. +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseInFilters) { + CollectionReference collection = Collection( + {{"a", {{"zip", FieldValue::Integer(98101)}}}, + {"b", {{"zip", FieldValue::Integer(98102)}}}, + {"c", {{"zip", FieldValue::Integer(98103)}}}, + {"d", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"e", + {{"zip", + FieldValue::Array( + {FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer(98101)}})})}}}, + {"f", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}}); + // Search for zips matching 98101, 98103, or [98101, 98102]. + QuerySnapshot snapshot = ReadDocuments(collection.WhereIn( + "zip", {FieldValue::Integer(98101), FieldValue::Integer(98103), + FieldValue::Array( + {FieldValue::Integer(98101), FieldValue::Integer(98102)})})); + EXPECT_EQ(std::vector( + {{{"zip", FieldValue::Integer(98101)}}, + {{"zip", FieldValue::Integer(98103)}}, + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}), + QuerySnapshotToValues(snapshot)); + + // With objects. + snapshot = ReadDocuments(collection.WhereIn( + "zip", {FieldValue::Map({{"code", FieldValue::Integer(500)}})})); + EXPECT_EQ( + std::vector( + {{{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseInFiltersWithDocIds) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + QuerySnapshot snapshot = ReadDocuments( + collection.WhereIn(FieldPath::DocumentId(), + {FieldValue::String("aa"), FieldValue::String("ab")})); + EXPECT_EQ(std::vector({{{"key", FieldValue::String("aa")}}, + {{"key", FieldValue::String("ab")}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseArrayContainsAnyFilters) { + CollectionReference collection = Collection( + {{"a", {{"array", FieldValue::Array({FieldValue::Integer(42)})}}}, + {"b", + {{"array", + FieldValue::Array({FieldValue::String("a"), FieldValue::Integer(42), + FieldValue::String("c")})}}}, + {"c", + {{"array", + FieldValue::Array( + {FieldValue::Double(41.999), FieldValue::String("42"), + FieldValue::Map( + {{"a", FieldValue::Array({FieldValue::Integer(42)})}})})}}}, + {"d", + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}, + {"e", {{"array", FieldValue::Array({FieldValue::Integer(43)})}}}, + {"f", + {{"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::Integer(42)}})})}}}, + {"g", {{"array", FieldValue::Integer(42)}}}}); + + // Search for "array" to contain [42, 43] + QuerySnapshot snapshot = ReadDocuments(collection.WhereArrayContainsAny( + "array", {FieldValue::Integer(42), FieldValue::Integer(43)})); + EXPECT_EQ(std::vector( + {{{"array", FieldValue::Array({FieldValue::Integer(42)})}}, + {{"array", FieldValue::Array({FieldValue::String("a"), + FieldValue::Integer(42), + FieldValue::String("c")})}}, + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}, + {{"array", FieldValue::Array({FieldValue::Integer(43)})}}}), + QuerySnapshotToValues(snapshot)); + + // With objects + snapshot = ReadDocuments(collection.WhereArrayContainsAny( + "array", {FieldValue::Map({{"a", FieldValue::Integer(42)}})})); + EXPECT_EQ(std::vector( + {{{"array", FieldValue::Array({FieldValue::Map( + {{"a", FieldValue::Integer(42)}})})}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestCollectionGroupQueries) { + Firestore* db = firestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "abc/123/" + collection_group + "/cg-doc1", + "abc/123/" + collection_group + "/cg-doc2", + collection_group + "/cg-doc3", + collection_group + "/cg-doc4", + "def/456/" + collection_group + "/cg-doc5", + collection_group + "/virtual-doc/nested-coll/not-cg-doc", + "x" + collection_group + "/not-cg-doc", + collection_group + "x/not-cg-doc", + "abc/123/" + collection_group + "x/not-cg-doc", + "abc/123/x" + collection_group + "/not-cg-doc", + "abc/" + collection_group, + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + QuerySnapshot query_snapshot = + ReadDocuments(db->CollectionGroup(collection_group)); + EXPECT_EQ(std::vector( + {"cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5"}), + QuerySnapshotToIds(query_snapshot)); +} + +TEST_F(FirestoreIntegrationTest, + TestCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds) { + Firestore* db = firestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "a/a/" + collection_group + "/cg-doc1", + "a/b/a/b/" + collection_group + "/cg-doc2", + "a/b/" + collection_group + "/cg-doc3", + "a/b/c/d/" + collection_group + "/cg-doc4", + "a/c/" + collection_group + "/cg-doc5", + collection_group + "/cg-doc6", + "a/b/nope/nope", + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + QuerySnapshot query_snapshot = + ReadDocuments(db->CollectionGroup(collection_group) + .OrderBy(FieldPath::DocumentId()) + .StartAt({FieldValue::String("a/b")}) + .EndAt({FieldValue::String("a/b0")})); + EXPECT_EQ(std::vector({"cg-doc2", "cg-doc3", "cg-doc4"}), + QuerySnapshotToIds(query_snapshot)); +} + +TEST_F(FirestoreIntegrationTest, + TestCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds) { + Firestore* db = firestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "a/a/" + collection_group + "/cg-doc1", + "a/b/a/b/" + collection_group + "/cg-doc2", + "a/b/" + collection_group + "/cg-doc3", + "a/b/c/d/" + collection_group + "/cg-doc4", + "a/c/" + collection_group + "/cg-doc5", + collection_group + "/cg-doc6", + "a/b/nope/nope", + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + QuerySnapshot query_snapshot = + ReadDocuments(db->CollectionGroup(collection_group) + .WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("a/b")) + .WhereLessThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("a/b0"))); + EXPECT_EQ(std::vector({"cg-doc2", "cg-doc3", "cg-doc4"}), + QuerySnapshotToIds(query_snapshot)); + + query_snapshot = ReadDocuments( + db->CollectionGroup(collection_group) + .WhereGreaterThan(FieldPath::DocumentId(), FieldValue::String("a/b")) + .WhereLessThan( + FieldPath::DocumentId(), + FieldValue::String("a/b/" + collection_group + "/cg-doc3"))); + EXPECT_EQ(std::vector({"cg-doc2"}), + QuerySnapshotToIds(query_snapshot)); +} + +#endif // !defined(FIRESTORE_STUB_BUILD) + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) +TEST_F(QueryTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(QueryTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/sanity_test.cc b/firestore/src/tests/sanity_test.cc new file mode 100644 index 0000000000..0fdc4be14b --- /dev/null +++ b/firestore/src/tests/sanity_test.cc @@ -0,0 +1,38 @@ +// This is a sanity test using gtest. The goal of this test is to make sure the +// way we setup Android C++ test harness actually works. We write test in a +// cross-platform way with gtest and run test with Android JUnit4 test runner +// for Android. We want this sanity test be as simple as possible while using +// the most critical mechanism of gtest. We also print information to stdout +// for debugging if anything goes wrong. + +#include +#include +#include "gtest/gtest.h" + +class SanityTest : public testing::Test { + protected: + void SetUp() override { printf("==== SetUp ====\n"); } + void TearDown() override { printf("==== TearDown ====\n"); } +}; + +// So far, Android native method cannot be inside namespace. So this has to be +// defined outside of any namespace. +TEST_F(SanityTest, TestSanity) { + printf("==== running %s ====\n", __PRETTY_FUNCTION__); + EXPECT_TRUE(true); +} + +TEST_F(SanityTest, TestAnotherSanity) { + printf("==== running %s ====\n", __PRETTY_FUNCTION__); + EXPECT_EQ(1, 1); +} + +// Generally we do not put test inside #if's because Android test harness will +// generate JUnit test whether macro is true or false. It is fine here since the +// test is enabled for Android. +#if __cpp_exceptions +TEST_F(SanityTest, TestThrow) { + printf("==== running %s ====\n", __PRETTY_FUNCTION__); + EXPECT_ANY_THROW({ throw "exception"; }); +} +#endif // __cpp_exceptions diff --git a/firestore/src/tests/server_timestamp_test.cc b/firestore/src/tests/server_timestamp_test.cc new file mode 100644 index 0000000000..1e7880feec --- /dev/null +++ b/firestore/src/tests/server_timestamp_test.cc @@ -0,0 +1,294 @@ +#include +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ServerTimestampTest.java + +namespace firebase { +namespace firestore { + +using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior; + +class ServerTimestampTest : public FirestoreIntegrationTest { + public: + ~ServerTimestampTest() override {} + + protected: + void SetUp() override { + doc_ = Document(); + listener_registration_ = + accumulator_.listener()->AttachTo(&doc_, MetadataChanges::kInclude); + + // Wait for initial null snapshot to avoid potential races. + DocumentSnapshot initial_snapshot = accumulator_.Await(); + EXPECT_FALSE(initial_snapshot.exists()); + } + + void TearDown() override { listener_registration_.Remove(); } + + /** Returns the expected data, with the specified timestamp substituted in. */ + MapFieldValue ExpectedDataWithTimestamp(const FieldValue& timestamp) { + return MapFieldValue{{"a", FieldValue::Integer(42)}, + {"when", timestamp}, + {"deep", FieldValue::Map({{"when", timestamp}})}}; + } + + /** Writes initial_data_ and waits for the corresponding snapshot. */ + void WriteInitialData() { + WriteDocument(doc_, initial_data_); + DocumentSnapshot initial_data_snapshot = accumulator_.Await(); + EXPECT_THAT(initial_data_snapshot.GetData(), + testing::ContainerEq(initial_data_)); + initial_data_snapshot = accumulator_.Await(); + EXPECT_THAT(initial_data_snapshot.GetData(), + testing::ContainerEq(initial_data_)); + } + + /** + * Verifies a snapshot containing set_data_ but with null for the timestamps. + */ + void VerifyTimestampsAreNull(const DocumentSnapshot& snapshot) { + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(ExpectedDataWithTimestamp(FieldValue::Null()))); + } + + /** + * Verifies a snapshot containing set_data_ but with resolved server + * timestamps. + */ + void VerifyTimestampsAreResolved(const DocumentSnapshot& snapshot) { + ASSERT_TRUE(snapshot.exists()); + ASSERT_TRUE(snapshot.Get("when").is_timestamp()); + Timestamp when = snapshot.Get("when").timestamp_value(); + // Tolerate up to 48*60*60 seconds of clock skew between client and server. + // This should be more than enough to compensate for timezone issues (even + // after taking daylight saving into account) and should allow local clocks + // to deviate from true time slightly and still pass the test. PORT_NOTE: + // For the tolerance here, Android uses 48*60*60 seconds while iOS uses 10 + // seconds. + int delta_sec = 48 * 60 * 60; + Timestamp now = Timestamp::Now(); + EXPECT_LT(abs(when.seconds() - now.seconds()), delta_sec) + << "resolved timestamp (" << when.ToString() << ") should be within " + << delta_sec << "s of now (" << now.ToString() << ")"; + + // Validate the rest of the document. + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq( + ExpectedDataWithTimestamp(FieldValue::Timestamp(when)))); + } + + /** + * Verifies a snapshot containing set_data_ but with local estimates for + * server timestamps. + */ + void VerifyTimestampsAreEstimates(const DocumentSnapshot& snapshot) { + ASSERT_TRUE(snapshot.exists()); + FieldValue when = snapshot.Get("when", ServerTimestampBehavior::kEstimate); + ASSERT_TRUE(when.is_timestamp()); + EXPECT_THAT(snapshot.GetData(ServerTimestampBehavior::kEstimate), + testing::ContainerEq(ExpectedDataWithTimestamp(when))); + } + + /** + * Verifies a snapshot containing set_data_ but using the previous field value + * for server timestamps. + */ + void VerifyTimestampsUsePreviousValue(const DocumentSnapshot& snapshot, + const FieldValue& previous) { + ASSERT_TRUE(snapshot.exists()); + ASSERT_TRUE(previous.is_null() || previous.is_timestamp()); + EXPECT_THAT(snapshot.GetData(ServerTimestampBehavior::kPrevious), + testing::ContainerEq(ExpectedDataWithTimestamp(previous))); + } + + // Data written in tests via set. + const MapFieldValue set_data_ = MapFieldValue{ + {"a", FieldValue::Integer(42)}, + {"when", FieldValue::ServerTimestamp()}, + {"deep", FieldValue::Map({{"when", FieldValue::ServerTimestamp()}})}}; + + // Base and update data used for update tests. + const MapFieldValue initial_data_ = + MapFieldValue{{"a", FieldValue::Integer(42)}}; + const MapFieldValue update_data_ = MapFieldValue{ + {"when", FieldValue::ServerTimestamp()}, + {"deep", FieldValue::Map({{"when", FieldValue::ServerTimestamp()}})}}; + + // A document reference to read and write to. + DocumentReference doc_; + + // Accumulator used to capture events during the test. + EventAccumulator accumulator_; + + // Listener registration for a listener maintained during the course of the + // test. + ListenerRegistration listener_registration_; +}; + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaSet) { + WriteDocument(doc_, set_data_); + VerifyTimestampsAreNull(accumulator_.AwaitLocalEvent()); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaUpdate) { + WriteInitialData(); + UpdateDocument(doc_, update_data_); + VerifyTimestampsAreNull(accumulator_.AwaitLocalEvent()); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsCanReturnEstimatedValue) { + WriteDocument(doc_, set_data_); + VerifyTimestampsAreEstimates(accumulator_.AwaitLocalEvent()); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsCanReturnPreviousValue) { + WriteDocument(doc_, set_data_); + VerifyTimestampsUsePreviousValue(accumulator_.AwaitLocalEvent(), + FieldValue::Null()); + DocumentSnapshot previous_snapshot = accumulator_.AwaitRemoteEvent(); + VerifyTimestampsAreResolved(previous_snapshot); + + UpdateDocument(doc_, update_data_); + VerifyTimestampsUsePreviousValue(accumulator_.AwaitLocalEvent(), + previous_snapshot.Get("when")); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsCanReturnPreviousValueOfDifferentType) { + WriteInitialData(); + UpdateDocument(doc_, MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + + DocumentSnapshot local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(local_snapshot.Get("a").is_null()); + EXPECT_TRUE(local_snapshot.Get("a", ServerTimestampBehavior::kEstimate) + .is_timestamp()); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + DocumentSnapshot remote_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(remote_snapshot.Get("a").is_timestamp()); + EXPECT_TRUE(remote_snapshot.Get("a", ServerTimestampBehavior::kEstimate) + .is_timestamp()); + EXPECT_TRUE(remote_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .is_timestamp()); +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsCanRetainPreviousValueThroughConsecutiveUpdates) { + WriteInitialData(); + Await(firestore()->DisableNetwork()); + accumulator_.AwaitRemoteEvent(); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + DocumentSnapshot local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + Await(firestore()->EnableNetwork()); + + DocumentSnapshot remote_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(remote_snapshot.Get("a").is_timestamp()); +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsUsesPreviousValueFromLocalMutation) { + WriteInitialData(); + Await(firestore()->DisableNetwork()); + accumulator_.AwaitRemoteEvent(); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + DocumentSnapshot local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + doc_.Update(MapFieldValue{{"a", FieldValue::Integer(1337)}}); + accumulator_.AwaitLocalEvent(); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(1337, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + Await(firestore()->EnableNetwork()); + + DocumentSnapshot remote_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(remote_snapshot.Get("a").is_timestamp()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaTransactionSet) { +#if defined(FIREBASE_USE_STD_FUNCTION) + Await(firestore()->RunTransaction( + [this](Transaction& transaction, std::string&) -> Error { + transaction.Set(doc_, set_data_); + return Error::kErrorOk; + })); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaTransactionUpdate) { +#if defined(FIREBASE_USE_STD_FUNCTION) + WriteInitialData(); + Await(firestore()->RunTransaction( + [this](Transaction& transaction, std::string&) -> Error { + transaction.Update(doc_, update_data_); + return Error::kErrorOk; + })); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsFailViaTransactionUpdateOnNonexistentDocument) { +#if defined(FIREBASE_USE_STD_FUNCTION) + Future future = firestore()->RunTransaction( + [this](Transaction& transaction, std::string&) -> Error { + transaction.Update(doc_, update_data_); + return Error::kErrorOk; + }); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsFailViaUpdateOnNonexistentDocument) { + Future future = doc_.Update(update_data_); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/smoke_test.cc b/firestore/src/tests/smoke_test.cc new file mode 100644 index 0000000000..6466a08e32 --- /dev/null +++ b/firestore/src/tests/smoke_test.cc @@ -0,0 +1,165 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FSTSmokeTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/SmokeTest.java + +namespace firebase { +namespace firestore { + +using TypeTest = FirestoreIntegrationTest; + +TEST_F(TypeTest, TestCanWriteASingleDocument) { + const MapFieldValue test_data{ + {"name", FieldValue::String("Patryk")}, + {"message", FieldValue::String("We are actually writing data!")}}; + CollectionReference collection = Collection(); + Await(collection.Add(test_data)); +} + +TEST_F(TypeTest, TestCanReadAWrittenDocument) { + const MapFieldValue test_data{{"foo", FieldValue::String("bar")}}; + CollectionReference collection = Collection(); + + DocumentReference new_reference = *Await(collection.Add(test_data)); + DocumentSnapshot result = *Await(new_reference.Get()); + EXPECT_THAT( + result.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +} + +TEST_F(TypeTest, TestObservesExistingDocument) { + const MapFieldValue test_data{{"foo", FieldValue::String("bar")}}; + DocumentReference writer_reference = + CachedFirestore("writer")->Collection("collection").Document(); + DocumentReference reader_reference = CachedFirestore("reader") + ->Collection("collection") + .Document(writer_reference.id()); + Await(writer_reference.Set(test_data)); + + EventAccumulator accumulator; + ListenerRegistration registration = accumulator.listener()->AttachTo( + &reader_reference, MetadataChanges::kInclude); + + DocumentSnapshot doc = accumulator.Await(); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); + registration.Remove(); +} + +TEST_F(TypeTest, TestObservesNewDocument) { + CollectionReference collection = Collection(); + DocumentReference writer_reference = collection.Document(); + DocumentReference reader_reference = + collection.Document(writer_reference.id()); + + EventAccumulator accumulator; + ListenerRegistration registration = accumulator.listener()->AttachTo( + &reader_reference, MetadataChanges::kInclude); + + DocumentSnapshot doc = accumulator.Await(); + EXPECT_FALSE(doc.exists()); + + const MapFieldValue test_data{{"foo", FieldValue::String("bar")}}; + Await(writer_reference.Set(test_data)); + + doc = accumulator.Await(); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); + EXPECT_TRUE(doc.metadata().has_pending_writes()); + + doc = accumulator.Await(); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); + EXPECT_FALSE(doc.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(TypeTest, TestWillFireValueEventsForEmptyCollections) { + CollectionReference collection = Collection(); + EventAccumulator accumulator; + ListenerRegistration registration = + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + + QuerySnapshot query_snapshot = accumulator.Await(); + EXPECT_EQ(0, query_snapshot.size()); + EXPECT_TRUE(query_snapshot.empty()); + + registration.Remove(); +} + +TEST_F(TypeTest, TestGetCollectionQuery) { + const std::map test_data{ + {"1", + {{"name", FieldValue::String("Patryk")}, + {"message", FieldValue::String("Real data, yo!")}}}, + {"2", + {{"name", FieldValue::String("Gil")}, + {"message", FieldValue::String("Yep!")}}}, + {"3", + {{"name", FieldValue::String("Jonny")}, + {"message", FieldValue::String("Back to work!")}}}}; + CollectionReference collection = Collection(test_data); + QuerySnapshot result = *Await(collection.Get()); + EXPECT_FALSE(result.empty()); + EXPECT_THAT( + QuerySnapshotToValues(result), + testing::ElementsAre( + MapFieldValue{{"name", FieldValue::String("Patryk")}, + {"message", FieldValue::String("Real data, yo!")}}, + MapFieldValue{{"name", FieldValue::String("Gil")}, + {"message", FieldValue::String("Yep!")}}, + MapFieldValue{{"name", FieldValue::String("Jonny")}, + {"message", FieldValue::String("Back to work!")}})); +} + +// TODO(klimt): This test is disabled because we can't create compound indexes +// programmatically. +TEST_F(TypeTest, DISABLED_TestQueryByFieldAndUseOrderBy) { + const std::map test_data{ + {"1", + {{"sort", FieldValue::Double(1.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("1")}}}, + {"2", + {{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("2")}}}, + {"3", + {{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("3")}}}, + {"4", + {{"sort", FieldValue::Double(3.0)}, + {"filter", FieldValue::Boolean(false)}, + {"key", FieldValue::String("4")}}}}; + CollectionReference collection = Collection(test_data); + Query query = collection.WhereEqualTo("filter", FieldValue::Boolean(true)) + .OrderBy("sort", Query::Direction::kDescending); + QuerySnapshot result = *Await(query.Get()); + EXPECT_THAT( + QuerySnapshotToValues(result), + testing::ElementsAre(MapFieldValue{{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("2")}}, + MapFieldValue{{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("3")}}, + MapFieldValue{{"sort", FieldValue::Double(1.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("1")}})); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/transaction_extra_test.cc b/firestore/src/tests/transaction_extra_test.cc new file mode 100644 index 0000000000..fad6361b17 --- /dev/null +++ b/firestore/src/tests/transaction_extra_test.cc @@ -0,0 +1,114 @@ +#include "app/src/time.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/firestore_errors.h" +#if defined(__ANDROID__) +#include "firestore/src/android/transaction_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/transaction_stub.h" +#endif // defined(__ANDROID__) + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FSTTransactionTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/TransactionTest.java + +namespace firebase { +namespace firestore { + +// We will be using lambda in the test instead of defining a +// TransactionFunction for each of the test case. +// +// We do have a TransactionFunction-version of the test +// TestGetNonexistentDocumentThenCreate to test the non-lambda API. + +using TransactionExtraTest = FirestoreIntegrationTest; + +#if defined(FIREBASE_USE_STD_FUNCTION) + +TEST_F(TransactionExtraTest, + TestRetriesWhenDocumentThatWasReadWithoutBeingWrittenChanges) { + DocumentReference doc1 = firestore()->Collection("counter").Document(); + DocumentReference doc2 = firestore()->Collection("counter").Document(); + WriteDocument(doc1, MapFieldValue{{"count", FieldValue::Integer(15)}}); + // Use these two as a portable way to mimic atomic integer. + Mutex mutex; + int transaction_runs_count = 0; + + Future future = firestore()->RunTransaction([&doc1, &doc2, &mutex, + &transaction_runs_count]( + Transaction& + transaction, + std::string& + error_message) + -> Error { + { + MutexLock lock(mutex); + ++transaction_runs_count; + } + // Get the first doc. + Error error = Error::kErrorOk; + DocumentSnapshot snapshot1 = transaction.Get(doc1, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + // Do a write outside of the transaction. The first time the + // transaction is tried, this will bump the version, which + // will cause the write to doc2 to fail. The second time, it + // will be a no-op and not bump the version. + // Now try to update the other doc from within the transaction. + Await(doc1.Set(MapFieldValue{{"count", FieldValue::Integer(1234)}})); + // Now try to update the other doc from within the transaction. + // This should fail once, because we read 15 earlier. + transaction.Set(doc2, MapFieldValue{{"count", FieldValue::Integer(16)}}); + return Error::kErrorOk; + }); + Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()); + EXPECT_EQ(2, transaction_runs_count); + DocumentSnapshot snapshot = ReadDocument(doc1); + EXPECT_EQ(1234, snapshot.Get("count").integer_value()); +} + +TEST_F(TransactionExtraTest, TestReadingADocTwiceWithDifferentVersions) { + int counter = 0; + DocumentReference doc = firestore()->Collection("counters").Document(); + WriteDocument(doc, MapFieldValue{{"count", FieldValue::Double(15.0)}}); + + Future future = firestore()->RunTransaction( + [&doc, &counter](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + // Get the doc once. + DocumentSnapshot snapshot1 = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + // Do a write outside of the transaction. Because the transaction will + // retry, set the document to a different value each time. + Await(doc.Set( + MapFieldValue{{"count", FieldValue::Double(1234.0 + counter)}})); + ++counter; + // Get the doc again in the transaction with the new version. + DocumentSnapshot snapshot2 = + transaction.Get(doc, &error, &error_message); + // We cannot check snapshot2, which is invalid as the second read would + // have already failed. + + // Now try to update the doc from within the transaction. + // This should fail, because we read 15 earlier. + transaction.Set(doc, + MapFieldValue{{"count", FieldValue::Double(16.0)}}); + return error; + }); + Await(future); + EXPECT_EQ(Error::kErrorAborted, future.error()); + EXPECT_STREQ("Document version changed between two reads.", + future.error_message()); + + DocumentSnapshot snapshot = ReadDocument(doc); +} + +#endif // defined(FIREBASE_USE_STD_FUNCTION) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/transaction_test.cc b/firestore/src/tests/transaction_test.cc new file mode 100644 index 0000000000..09cb0d70ca --- /dev/null +++ b/firestore/src/tests/transaction_test.cc @@ -0,0 +1,750 @@ +#include +#include + +#if !defined(FIRESTORE_STUB_BUILD) +#include "app/src/mutex.h" +#include "app/src/semaphore.h" +#include "app/src/time.h" +#endif + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "absl/strings/str_join.h" +#include "firebase/firestore/firestore_errors.h" +#if defined(__ANDROID__) +#include "firestore/src/android/transaction_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/transaction_stub.h" + +#endif // defined(__ANDROID__) + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FSTTransactionTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/TransactionTest.java +// +// Some test cases are moved to transaction_extra_test.cc. If run together, the +// test will run too long and timeout. + +namespace firebase { +namespace firestore { + +// These tests don't work with the stubs. +#if !defined(FIRESTORE_STUB_BUILD) + +using ::testing::HasSubstr; + +// We will be using lambda in the test instead of defining a +// TransactionFunction for each of the test case. +// +// We do have a TransactionFunction-version of the test +// TestGetNonexistentDocumentThenCreate to test the non-lambda API. + +class TransactionTest : public FirestoreIntegrationTest { + protected: +#if defined(FIREBASE_USE_STD_FUNCTION) + // We occasionally get transient error like "Could not reach Cloud Firestore + // backend. Backend didn't respond within 10 seconds". Transaction requires + // online and thus will not retry. So we do the retry in the testcase. + void RunTransactionAndExpect( + Error error, const char* message, + std::function update) { + Future future; + // Re-try 5 times in case server is unavailable. + for (int i = 0; i < 5; ++i) { + future = firestore()->RunTransaction(update); + Await(future); + if (future.error() == Error::kErrorUnavailable) { + std::cout << "Could not reach backend. Retrying transaction test." + << std::endl; + } else { + break; + } + } + EXPECT_EQ(error, future.error()); + EXPECT_THAT(future.error_message(), HasSubstr(message)); + } + + void RunTransactionAndExpect( + Error error, std::function update) { + switch (error) { + case Error::kErrorOk: + RunTransactionAndExpect(Error::kErrorOk, "", std::move(update)); + break; + case Error::kErrorAborted: + RunTransactionAndExpect( +#if defined(__APPLE__) + Error::kErrorFailedPrecondition, +#else + Error::kErrorAborted, +#endif + "Transaction failed all retries.", std::move(update)); + break; + case Error::kErrorFailedPrecondition: + // Here specifies error message of the most common cause. There are + // other causes for FailedPrecondition as well. Use the one with message + // parameter if the expected error message is different. + RunTransactionAndExpect(Error::kErrorFailedPrecondition, + "Can't update a document that doesn't exist.", + std::move(update)); + break; + default: + FAIL() << "Unexpected error code: " << error; + } + } +#endif // defined(FIREBASE_USE_STD_FUNCTION) +}; + +class TestTransactionFunction : public TransactionFunction { + public: + TestTransactionFunction(DocumentReference doc) : doc_(doc) {} + + Error Apply(Transaction& transaction, std::string& error_message) override { + Error error = Error::kErrorUnknown; + DocumentSnapshot snapshot = transaction.Get(doc_, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + EXPECT_FALSE(snapshot.exists()); + transaction.Set(doc_, MapFieldValue{{key_, FieldValue::String(value_)}}); + return error; + } + + std::string key() { return key_; } + std::string value() { return value_; } + + private: + DocumentReference doc_; + const std::string key_{"foo"}; + const std::string value_{"bar"}; +}; + +TEST_F(TransactionTest, TestGetNonexistentDocumentThenCreatePortableVersion) { + DocumentReference doc = firestore()->Collection("towns").Document(); + TestTransactionFunction transaction{doc}; + Future future = firestore()->RunTransaction(&transaction); + Await(future); + + EXPECT_EQ(Error::kErrorOk, future.error()); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_EQ(FieldValue::String(transaction.value()), + snapshot.Get(transaction.key())); +} + +#if defined(FIREBASE_USE_STD_FUNCTION) + +class TransactionStage { + public: + TransactionStage( + std::string tag, + std::function func) + : tag_(std::move(tag)), func_(std::move(func)) {} + + const std::string& tag() const { return tag_; } + + void operator()(Transaction* transaction, + const DocumentReference& doc) const { + func_(transaction, doc); + } + + bool operator==(const TransactionStage& rhs) const { + return tag_ == rhs.tag_; + } + + bool operator!=(const TransactionStage& rhs) const { + return tag_ != rhs.tag_; + } + + private: + std::string tag_; + std::function func_; +}; + +/** + * The transaction stages that follow are postfixed by numbers to indicate the + * calling order. For example, calling `set1` followed by `set2` should result + * in the document being set to the value specified by `set2`. + */ +const auto delete1 = new TransactionStage( + "delete", [](Transaction* transaction, const DocumentReference& doc) { + transaction->Delete(doc); + }); + +const auto update1 = new TransactionStage("update", [](Transaction* transaction, + const DocumentReference& + doc) { + transaction->Update(doc, MapFieldValue{{"foo", FieldValue::String("bar1")}}); +}); + +const auto update2 = new TransactionStage("update", [](Transaction* transaction, + const DocumentReference& + doc) { + transaction->Update(doc, MapFieldValue{{"foo", FieldValue::String("bar2")}}); +}); + +const auto set1 = new TransactionStage( + "set", [](Transaction* transaction, const DocumentReference& doc) { + transaction->Set(doc, MapFieldValue{{"foo", FieldValue::String("bar1")}}); + }); + +const auto set2 = new TransactionStage( + "set", [](Transaction* transaction, const DocumentReference& doc) { + transaction->Set(doc, MapFieldValue{{"foo", FieldValue::String("bar2")}}); + }); + +const auto get = new TransactionStage( + "get", [](Transaction* transaction, const DocumentReference& doc) { + Error error; + std::string msg; + transaction->Get(doc, &error, &msg); + }); + +/** + * Used for testing that all possible combinations of executing transactions + * result in the desired document value or error. + * + * `Run()`, `WithExistingDoc()`, and `WithNonexistentDoc()` don't actually do + * anything except assign variables into the `TransactionTester`. + * + * `ExpectDoc()`, `ExpectNoDoc()`, and `ExpectError()` will trigger the + * transaction to run and assert that the end result matches the input. + */ +class TransactionTester { + public: + explicit TransactionTester(Firestore* db) : db_(db) {} + + template + TransactionTester& Run(Args... args) { + stages_ = {*args...}; + return *this; + } + + TransactionTester& WithExistingDoc() { + from_existing_doc_ = true; + return *this; + } + + TransactionTester& WithNonexistentDoc() { + from_existing_doc_ = false; + return *this; + } + + void ExpectDoc(const MapFieldValue& expected) { + PrepareDoc(); + RunSuccessfulTransaction(); + Future future = doc_.Get(); + const DocumentSnapshot* snapshot = FirestoreIntegrationTest::Await(future); + EXPECT_TRUE(snapshot->exists()); + EXPECT_THAT(snapshot->GetData(), expected); + stages_.clear(); + } + + void ExpectNoDoc() { + PrepareDoc(); + RunSuccessfulTransaction(); + Future future = doc_.Get(); + const DocumentSnapshot* snapshot = FirestoreIntegrationTest::Await(future); + EXPECT_FALSE(snapshot->exists()); + stages_.clear(); + } + + void ExpectError(Error error) { + PrepareDoc(); + RunFailingTransaction(error); + stages_.clear(); + } + + private: + void PrepareDoc() { + doc_ = db_->Collection("tx-tester").Document(); + if (from_existing_doc_) { + FirestoreIntegrationTest::Await( + doc_.Set(MapFieldValue{{"foo", FieldValue::String("bar0")}})); + } + } + + void RunSuccessfulTransaction() { + Future future = db_->RunTransaction( + [this](Transaction& transaction, std::string& error_message) { + for (const auto& stage : stages_) { + stage(&transaction, doc_); + } + return Error::kErrorOk; + }); + FirestoreIntegrationTest::Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()) + << "Expected the sequence (" + ListStages() + ") to succeed, but got " + + std::to_string(future.error()); + } + + void RunFailingTransaction(Error error) { + Future future = db_->RunTransaction( + [this](Transaction& transaction, std::string& error_message) { + for (const auto& stage : stages_) { + stage(&transaction, doc_); + } + return Error::kErrorOk; + }); + FirestoreIntegrationTest::Await(future); + EXPECT_EQ(error, future.error()) + << "Expected the sequence (" + ListStages() + + ") to fail with the error " + std::to_string(error); + } + + std::string ListStages() const { + std::vector stages; + for (const auto& stage : stages_) { + stages.push_back(stage.tag()); + } + return absl::StrJoin(stages, ","); + } + + Firestore* db_ = nullptr; + DocumentReference doc_; + bool from_existing_doc_ = false; + std::vector stages_; +}; + +TEST_F(TransactionTest, TestRunsTransactionsAfterGettingNonexistentDoc) { + SCOPED_TRACE("TestRunsTransactionsAfterGettingNonexistentDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithExistingDoc().Run(get, delete1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(get, delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithExistingDoc() + .Run(get, delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(get, update1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(get, update1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(get, update1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(get, set1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(get, set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(get, set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestRunsTransactionsAfterGettingExistingDoc) { + SCOPED_TRACE("TestRunsTransactionsAfterGettingExistingDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithNonexistentDoc().Run(get, delete1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(get, delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(get, delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithNonexistentDoc() + .Run(get, update1, delete1) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(get, update1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(get, update1, set2) + .ExpectError(Error::kErrorInvalidArgument); + + tt.WithNonexistentDoc().Run(get, set1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(get, set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithNonexistentDoc() + .Run(get, set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestRunsTransactionsOnExistingDoc) { + SCOPED_TRACE("TestRunTransactionsOnExistingDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithExistingDoc().Run(delete1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithExistingDoc() + .Run(get, delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(update1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(update1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(update1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(set1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestRunsTransactionsOnNonexistentDoc) { + SCOPED_TRACE("TestRunsTransactionsOnNonexistentDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithNonexistentDoc().Run(delete1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithNonexistentDoc() + .Run(update1, delete1) + .ExpectError(Error::kErrorNotFound); + tt.WithNonexistentDoc() + .Run(update1, update2) + .ExpectError(Error::kErrorNotFound); + tt.WithNonexistentDoc().Run(update1, set2).ExpectError(Error::kErrorNotFound); + + tt.WithNonexistentDoc().Run(set1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithNonexistentDoc() + .Run(set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestGetNonexistentDocumentThenFailPatch) { + DocumentReference doc = firestore()->Collection("towns").Document(); + + SCOPED_TRACE("TestGetNonexistentDocumentThenFailPatch"); + RunTransactionAndExpect( + Error::kErrorInvalidArgument, + "Can't update a document that doesn't exist.", + [doc](Transaction& transaction, std::string& error_message) -> Error { + Error error = Error::kErrorOk; + DocumentSnapshot snapshot = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + EXPECT_FALSE(snapshot.exists()); + transaction.Update(doc, + MapFieldValue{{"foo", FieldValue::String("bar")}}); + return error; + }); +} + +TEST_F(TransactionTest, TestSetDocumentWithMerge) { + DocumentReference doc = firestore()->Collection("towns").Document(); + + SCOPED_TRACE("TestSetDocumentWithMerge"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Set( + doc, + MapFieldValue{{"a", FieldValue::String("b")}, + {"nested", FieldValue::Map(MapFieldValue{ + {"a", FieldValue::String("b")}})}}); + transaction.Set( + doc, + MapFieldValue{{"c", FieldValue::String("d")}, + {"nested", FieldValue::Map(MapFieldValue{ + {"c", FieldValue::String("d")}})}}, + SetOptions::Merge()); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("b")}, + {"c", FieldValue::String("d")}, + {"nested", FieldValue::Map(MapFieldValue{ + {"a", FieldValue::String("b")}, + {"c", FieldValue::String("d")}})}})); +} + +TEST_F(TransactionTest, TestCannotUpdateNonExistentDocument) { + DocumentReference doc = firestore()->Collection("towns").Document(); + + SCOPED_TRACE("TestCannotUpdateNonExistentDocument"); + RunTransactionAndExpect( + Error::kErrorNotFound, "", + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Update(doc, + MapFieldValue{{"foo", FieldValue::String("bar")}}); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(TransactionTest, TestIncrementTransactionally) { + // A set of concurrent transactions. + std::vector> transaction_tasks; + // A barrier to make sure every transaction reaches the same spot. + Semaphore write_barrier{0}; + // Use these two as a portable way to mimic atomic integer. + Mutex started_locker; + int started = 0; + + DocumentReference doc = firestore()->Collection("counters").Document(); + WriteDocument(doc, MapFieldValue{{"count", FieldValue::Double(5.0)}}); + + // Make 3 transactions that will all increment. + // Note: Visual Studio 2015 incorrectly requires `kTotal` to be captured in + // the lambda, even though it's a constant expression. Adding `static` as + // a workaround. + static constexpr int kTotal = 3; + for (int i = 0; i < kTotal; ++i) { + transaction_tasks.push_back(firestore()->RunTransaction( + [doc, &write_barrier, &started_locker, &started]( + Transaction& transaction, std::string& error_message) -> Error { + Error error = Error::kErrorOk; + DocumentSnapshot snapshot = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + { + MutexLock lock(started_locker); + ++started; + // Once all of the transactions have read, allow the first write. + if (started == kTotal) { + write_barrier.Post(); + } + } + + // Let all of the transactions fetch the old value and stop once. + write_barrier.Wait(); + // Refill the barrier so that the other transactions and retries + // succeed. + write_barrier.Post(); + + double new_count = snapshot.Get("count").double_value() + 1.0; + transaction.Set( + doc, MapFieldValue{{"count", FieldValue::Double(new_count)}}); + return error; + })); + } + + // Until we have another Await() that waits for multiple Futures, we do the + // wait in one by one. + while (!transaction_tasks.empty()) { + Future future = transaction_tasks.back(); + transaction_tasks.pop_back(); + Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()); + } + // Now all transaction should be completed, so check the result. + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_DOUBLE_EQ(5.0 + kTotal, snapshot.Get("count").double_value()); +} + +TEST_F(TransactionTest, TestUpdateTransactionally) { + // A set of concurrent transactions. + std::vector> transaction_tasks; + // A barrier to make sure every transaction reaches the same spot. + Semaphore write_barrier{0}; + // Use these two as a portable way to mimic atomic integer. + Mutex started_locker; + int started = 0; + + DocumentReference doc = firestore()->Collection("counters").Document(); + WriteDocument(doc, MapFieldValue{{"count", FieldValue::Double(5.0)}, + {"other", FieldValue::String("yes")}}); + + // Make 3 transactions that will all increment. + static const constexpr int kTotal = 3; + for (int i = 0; i < kTotal; ++i) { + transaction_tasks.push_back(firestore()->RunTransaction( + [doc, &write_barrier, &started_locker, &started]( + Transaction& transaction, std::string& error_message) -> Error { + Error error = Error::kErrorOk; + DocumentSnapshot snapshot = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + { + MutexLock lock(started_locker); + ++started; + // Once all of the transactions have read, allow the first write. + if (started == kTotal) { + write_barrier.Post(); + } + } + + // Let all of the transactions fetch the old value and stop once. + write_barrier.Wait(); + // Refill the barrier so that the other transactions and retries + // succeed. + write_barrier.Post(); + + double new_count = snapshot.Get("count").double_value() + 1.0; + transaction.Update( + doc, MapFieldValue{{"count", FieldValue::Double(new_count)}}); + return error; + })); + } + + // Until we have another Await() that waits for multiple Futures, we do the + // wait in backward order. + while (!transaction_tasks.empty()) { + Future future = transaction_tasks.back(); + transaction_tasks.pop_back(); + Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()); + } + // Now all transaction should be completed, so check the result. + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_DOUBLE_EQ(5.0 + kTotal, snapshot.Get("count").double_value()); + EXPECT_EQ("yes", snapshot.Get("other").string_value()); +} + +TEST_F(TransactionTest, TestUpdateFieldsWithDotsTransactionally) { + DocumentReference doc = firestore()->Collection("fieldnames").Document(); + WriteDocument(doc, MapFieldValue{{"a.b", FieldValue::String("old")}, + {"c.d", FieldValue::String("old")}, + {"e.f", FieldValue::String("old")}}); + + SCOPED_TRACE("TestUpdateFieldsWithDotsTransactionally"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Update(doc, MapFieldPathValue{{FieldPath{"a.b"}, + FieldValue::String("new")}}); + transaction.Update(doc, MapFieldPathValue{{FieldPath{"c.d"}, + FieldValue::String("new")}}); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a.b", FieldValue::String("new")}, + {"c.d", FieldValue::String("new")}, + {"e.f", FieldValue::String("old")}})); +} + +TEST_F(TransactionTest, TestUpdateNestedFieldsTransactionally) { + DocumentReference doc = firestore()->Collection("fieldnames").Document(); + WriteDocument( + doc, MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("old")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("old")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}}); + + SCOPED_TRACE("TestUpdateNestedFieldsTransactionally"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Update(doc, + MapFieldValue{{"a.b", FieldValue::String("new")}}); + transaction.Update(doc, + MapFieldValue{{"c.d", FieldValue::String("new")}}); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("new")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("new")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); +} + +#if defined(__ANDROID__) +// TODO(b/136012313): on iOS, this triggers assertion failure. +TEST_F(TransactionTest, TestCannotReadAfterWriting) { + DocumentReference doc = firestore()->Collection("anything").Document(); + DocumentSnapshot snapshot; + + SCOPED_TRACE("TestCannotReadAfterWriting"); + RunTransactionAndExpect( + Error::kErrorInvalidArgument, + "Firestore transactions require all reads to be " + "executed before all writes.", + [doc, &snapshot](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + transaction.Set(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + snapshot = transaction.Get(doc, &error, &error_message); + return error; + }); + + snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} +#endif + +TEST_F(TransactionTest, TestCanHaveGetsWithoutMutations) { + DocumentReference doc1 = firestore()->Collection("foo").Document(); + DocumentReference doc2 = firestore()->Collection("foo").Document(); + WriteDocument(doc1, MapFieldValue{{"foo", FieldValue::String("bar")}}); + DocumentSnapshot snapshot; + + SCOPED_TRACE("TestCanHaveGetsWithoutMutations"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc1, doc2, &snapshot](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + transaction.Get(doc2, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + snapshot = transaction.Get(doc1, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + return error; + }); + EXPECT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +} + +TEST_F(TransactionTest, TestSuccessWithNoTransactionOperations) { + SCOPED_TRACE("TestSuccessWithNoTransactionOperations"); + RunTransactionAndExpect( + Error::kErrorOk, + [](Transaction&, std::string&) -> Error { return Error::kErrorOk; }); +} + +TEST_F(TransactionTest, TestCancellationOnError) { + DocumentReference doc = firestore()->Collection("towns").Document(); + // Use these two as a portable way to mimic atomic integer. + Mutex count_locker; + int count = 0; + + SCOPED_TRACE("TestCancellationOnError"); + RunTransactionAndExpect( + Error::kErrorDeadlineExceeded, "no", + [doc, &count_locker, &count](Transaction& transaction, + std::string& error_message) -> Error { + { + MutexLock lock{count_locker}; + ++count; + } + transaction.Set(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + error_message = "no"; + return Error::kErrorDeadlineExceeded; + }); + + // TODO(varconst): uncomment. Currently, there is no way in C++ to distinguish + // user error, so the transaction gets retried, and the counter goes up to 6. + // EXPECT_EQ(1, count); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +#endif // defined(FIREBASE_USE_STD_FUNCTION) + +#endif // defined(__ANDROID__) || defined(__APPLE__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/type_test.cc b/firestore/src/tests/type_test.cc new file mode 100644 index 0000000000..40e005039f --- /dev/null +++ b/firestore/src/tests/type_test.cc @@ -0,0 +1,71 @@ +#include "app/src/log.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRTypeTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/TypeTest.java + +namespace firebase { +namespace firestore { + +class TypeTest : public FirestoreIntegrationTest { + public: + // Write the specified data to Firestore as a document and read that document. + // Check the data read from that document matches with the original data. + void AssertSuccessfulRoundTrip(MapFieldValue data) { + firestore()->set_log_level(LogLevel::kLogLevelDebug); + DocumentReference reference = firestore()->Document("rooms/eros"); + WriteDocument(reference, data); + DocumentSnapshot snapshot = ReadDocument(reference); + EXPECT_TRUE(snapshot.exists()); + EXPECT_EQ(snapshot.GetData(), data); + } +}; + +TEST_F(TypeTest, TestCanReadAndWriteNullFields) { + AssertSuccessfulRoundTrip( + {{"a", FieldValue::Integer(1)}, {"b", FieldValue::Null()}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteArrayFields) { + AssertSuccessfulRoundTrip( + {{"array", FieldValue::Array( + {FieldValue::Integer(1), FieldValue::String("foo"), + FieldValue::Map({{"deep", FieldValue::Boolean(true)}}), + FieldValue::Null()})}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteBlobFields) { + uint8_t blob[3] = {0, 1, 2}; + AssertSuccessfulRoundTrip({{"blob", FieldValue::Blob(blob, 3)}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteGeoPointFields) { + AssertSuccessfulRoundTrip({{"geoPoint", FieldValue::GeoPoint({1.23, 4.56})}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteDateFields) { + AssertSuccessfulRoundTrip( + {{"date", FieldValue::Timestamp(Timestamp::FromTimeT(1491847082))}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteTimestampFields) { + AssertSuccessfulRoundTrip( + {{"date", FieldValue::Timestamp({123456, 123456000})}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteDocumentReferences) { + AssertSuccessfulRoundTrip({{"a", FieldValue::Integer(42)}, + {"ref", FieldValue::Reference(Document())}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteDocumentReferencesInArrays) { + AssertSuccessfulRoundTrip( + {{"a", FieldValue::Integer(42)}, + {"refs", FieldValue::Array({FieldValue::Reference(Document())})}}); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/util/integration_test_util.cc b/firestore/src/tests/util/integration_test_util.cc new file mode 100644 index 0000000000..75039eff8f --- /dev/null +++ b/firestore/src/tests/util/integration_test_util.cc @@ -0,0 +1,66 @@ +#include // NOLINT(build/c++11) +#include // NOLINT(build/c++11) + +#include "devtools/build/runtime/get_runfiles_dir.h" +#include "app/src/include/firebase/app.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/ios/firestore_ios.h" +#include "firestore/src/ios/hard_assert_ios.h" +#include "absl/memory/memory.h" +#include "Firestore/core/src/auth/empty_credentials_provider.h" + +namespace firebase { +namespace firestore { + +using auth::EmptyCredentialsProvider; + +struct TestFriend { + static FirestoreInternal* CreateTestFirestoreInternal(App* app) { + return new FirestoreInternal(app, + absl::make_unique()); + } +}; + +App* GetApp(const char* name) { + // TODO(varconst): try to avoid using a real project ID when possible. iOS + // unit tests achieve this by using fake options: + // https://github.com/firebase/firebase-ios-sdk/blob/9a5afbffc17bb63b7bb7f51b9ea9a6a9e1c88a94/Firestore/core/test/firebase/firestore/testutil/app_testing.mm#L29 + + // Note: setting the default config path doesn't affect anything on iOS. + // This is done unconditionally to simplify the logic. + std::string google_json_dir = devtools_build::testonly::GetTestSrcdir() + + "/google3/firebase/firestore/client/cpp/"; + App::SetDefaultConfigPath(google_json_dir.c_str()); + + if (name == nullptr || std::string{name} == kDefaultAppName) { + return App::Create(); + } else { + App* default_app = App::GetInstance(); + HARD_ASSERT_IOS(default_app, + "Cannot create a named app before the default app"); + return App::Create(default_app->options(), name); + } +} + +App* GetApp() { return GetApp(nullptr); } + +// TODO(varconst): it's brittle and potentially flaky, look into using some +// notification mechanism instead. +bool ProcessEvents(int millis) { + std::this_thread::sleep_for(std::chrono::milliseconds(millis)); + // `false` means "don't shut down the application". + return false; +} + +FirestoreInternal* CreateTestFirestoreInternal(App* app) { + return TestFriend::CreateTestFirestoreInternal(app); +} + +#ifndef __APPLE__ +void InitializeFirestore(Firestore* instance) { + Firestore::set_log_level(LogLevel::kLogLevelDebug); +} +#endif // #ifndef __APPLE__ + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/util/integration_test_util_apple.mm b/firestore/src/tests/util/integration_test_util_apple.mm new file mode 100644 index 0000000000..5125f264c8 --- /dev/null +++ b/firestore/src/tests/util/integration_test_util_apple.mm @@ -0,0 +1,21 @@ +#include + +#include +#include + +#include "firestore/src/include/firebase/firestore.h" + +namespace firebase { +namespace firestore { + +// Note: currently, this file has to be Objective-C++ (`.mm`), because `Settings` are defined in +// such a way that configuring the dispatch queue is only possible within Objective-C++ translation +// units. +// TODO(varconst): fix this somehow. + +void InitializeFirestore(Firestore* instance) { + Firestore::set_log_level(LogLevel::kLogLevelDebug); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/validation_test.cc b/firestore/src/tests/validation_test.cc new file mode 100644 index 0000000000..2cb243a194 --- /dev/null +++ b/firestore/src/tests/validation_test.cc @@ -0,0 +1,885 @@ +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include "firestore/src/android/util_android.h" +#endif // defined(__ANDROID__) +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/firestore_errors.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRValidationTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ValidationTest.java +// +// PORT_NOTE: C++ API Guidelines (http://g3doc/firebase/g3doc/cpp-api-style.md) +// discourage the use of exceptions in the Firebase Games's SDK. So in release, +// we do not throw exception while only dump exception info to logs. However, in +// order to test this behavior, we enable exception here and check exceptions. + +namespace firebase { +namespace firestore { + +// This eventually works for iOS as well and becomes the cross-platform test for +// C++ client SDK. For now, only enabled for Android platform. + +#if defined(__ANDROID__) + +class ValidationTest : public FirestoreIntegrationTest { + protected: + /** + * Performs a write using each write API and makes sure it fails with the + * expected reason. + */ + void ExpectWriteError(const MapFieldValue& data, const std::string& reason) { + ExpectWriteError(data, reason, /*include_sets=*/true, + /*include_updates=*/true); + } + + /** + * Performs a write using each update API and makes sure it fails with the + * expected reason. + */ + void ExpectUpdateError(const MapFieldValue& data, const std::string& reason) { + ExpectWriteError(data, reason, /*include_sets=*/false, + /*include_updates=*/true); + } + + /** + * Performs a write using each set API and makes sure it fails with the + * expected reason. + */ + void ExpectSetError(const MapFieldValue& data, const std::string& reason) { + ExpectWriteError(data, reason, /*include_sets=*/true, + /*include_updates=*/false); + } + + /** + * Performs a write using each set and/or update API and makes sure it fails + * with the expected reason. + */ + void ExpectWriteError(const MapFieldValue& data, const std::string& reason, + bool include_sets, bool include_updates) { + DocumentReference document = Document(); + + if (include_sets) { + try { + document.Set(data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + firestore()->batch().Set(document, data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } + + if (include_updates) { + try { + document.Update(data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + firestore()->batch().Update(document, data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } + +#if defined(FIREBASE_USE_STD_FUNCTION) + Await(firestore()->RunTransaction( + [data, reason, include_sets, include_updates, document]( + Transaction& transaction, std::string& error_message) -> Error { + if (include_sets) { + transaction.Set(document, data); + } + if (include_updates) { + transaction.Update(document, data); + } + return Error::kErrorOk; + })); +#endif // defined(FIREBASE_USE_STD_FUNCTION) + } + + /** + * Tests a field path with all of our APIs that accept field paths and ensures + * they fail with the specified reason. + */ + // TODO(varconst): this function is pretty much commented out. + void VerifyFieldPathThrows(const std::string& path, + const std::string& reason) { + // Get an arbitrary snapshot we can use for testing. + DocumentReference document = Document(); + WriteDocument(document, MapFieldValue{{"test", FieldValue::Integer(1)}}); + DocumentSnapshot snapshot = ReadDocument(document); + + // snapshot paths + try { + // TODO(varconst): The logic is in the C++ core and is a hard assertion. + // snapshot.Get(path); + // FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + + // Query filter / order fields + CollectionReference collection = Collection(); + // WhereLessThan(), etc. omitted for brevity since the code path is + // trivially shared. + try { + // TODO(zxu): The logic is in the C++ core and is a hard assertion. + // collection.WhereEqualTo(path, FieldValue::Integer(1)); + // FAIL() << "should throw exception" << path; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + // TODO(zxu): The logic is in the C++ core and is a hard assertion. + // collection.OrderBy(path); + // FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + + // update() paths. + try { + document.Update(MapFieldValue{{path, FieldValue::Integer(1)}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } +}; + +// PORT_NOTE: Does not apply to C++ as host parameter is passed by value. +TEST_F(ValidationTest, FirestoreSettingsNullHostFails) {} + +TEST_F(ValidationTest, ChangingSettingsAfterUseFails) { + DocumentReference reference = Document(); + // Force initialization of the underlying client + WriteDocument(reference, MapFieldValue{{"key", FieldValue::String("value")}}); + Settings setting; + setting.set_host("foo"); + try { + firestore()->set_settings(setting); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "FirebaseFirestore has already been started and its settings can no " + "longer be changed. You can only call setFirestoreSettings() before " + "calling any other methods on a FirebaseFirestore object.", + exception.what()); + } +} + +TEST_F(ValidationTest, DisableSslWithoutSettingHostFails) { + Settings setting; + setting.set_ssl_enabled(false); + try { + firestore()->set_settings(setting); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "You can't set the 'sslEnabled' setting unless you also set a " + "non-default 'host'.", + exception.what()); + } +} + +// PORT_NOTE: Does not apply to C++ as host parameter is passed by value. +TEST_F(ValidationTest, FirestoreGetInstanceWithNullAppFails) {} + +TEST_F(ValidationTest, + FirestoreGetInstanceWithNonNullAppReturnsNonNullInstance) { + try { + InitResult result; + Firestore::GetInstance(app(), &result); + EXPECT_EQ(kInitResultSuccess, result); + } catch (const FirestoreException& exception) { + FAIL() << "shouldn't throw exception"; + } +} + +TEST_F(ValidationTest, CollectionPathsMustBeOddLength) { + Firestore* db = firestore(); + DocumentReference base_document = db->Document("foo/bar"); + std::vector bad_absolute_paths = {"foo/bar", "foo/bar/baz/quu"}; + std::vector bad_relative_paths = {"/", "baz/quu"}; + std::vector expect_errors = { + "Invalid collection reference. Collection references must have an odd " + "number of segments, but foo/bar has 2", + "Invalid collection reference. Collection references must have an odd " + "number of segments, but foo/bar/baz/quu has 4", + }; + for (int i = 0; i < expect_errors.size(); ++i) { + try { + db->Collection(bad_absolute_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + try { + base_document.Collection(bad_relative_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + } +} + +TEST_F(ValidationTest, PathsMustNotHaveEmptySegments) { + Firestore* db = firestore(); + // NOTE: leading / trailing slashes are okay. + db->Collection("/foo/"); + db->Collection("/foo"); + db->Collection("foo/"); + + std::vector bad_paths = {"foo//bar//baz", "//foo", "foo//"}; + CollectionReference collection = db->Collection("test-collection"); + DocumentReference document = collection.Document("test-document"); + for (const std::string& path : bad_paths) { + std::string reason = + "Invalid path (" + path + "). Paths must not contain // in them."; + try { + db->Collection(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + db->Document(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + collection.Document(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + document.Collection(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } +} + +TEST_F(ValidationTest, DocumentPathsMustBeEvenLength) { + Firestore* db = firestore(); + CollectionReference base_collection = db->Collection("foo"); + std::vector bad_absolute_paths = {"foo", "foo/bar/baz"}; + std::vector bad_relative_paths = {"/", "bar/baz"}; + std::vector expect_errors = { + "Invalid document reference. Document references must have an even " + "number of segments, but foo has 1", + "Invalid document reference. Document references must have an even " + "number of segments, but foo/bar/baz has 3", + }; + for (int i = 0; i < expect_errors.size(); ++i) { + try { + db->Document(bad_absolute_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + try { + base_collection.Document(bad_relative_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + } +} + +// PORT_NOTE: Does not apply to C++ which is strong-typed. +TEST_F(ValidationTest, WritesMustBeMapsOrPOJOs) {} + +TEST_F(ValidationTest, WritesMustNotContainDirectlyNestedLists) { + SCOPED_TRACE("WritesMustNotContainDirectlyNestedLists"); + + ExpectWriteError( + MapFieldValue{ + {"nested-array", + FieldValue::Array({FieldValue::Integer(1), + FieldValue::Array({FieldValue::Integer(2)})})}}, + "Invalid data. Nested arrays are not supported"); +} + +TEST_F(ValidationTest, WritesMayContainIndirectlyNestedLists) { + MapFieldValue data = { + {"nested-array", + FieldValue::Array( + {FieldValue::Integer(1), + FieldValue::Map({{"foo", FieldValue::Integer(2)}})})}}; + + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + DocumentReference another_document = collection.Document(); + + Await(document.Set(data)); + Await(firestore()->batch().Set(document, data).Commit()); + + Await(document.Update(data)); + Await(firestore()->batch().Update(document, data).Commit()); + +#if defined(FIREBASE_USE_STD_FUNCTION) + Await(firestore()->RunTransaction( + [data, document, another_document](Transaction& transaction, + std::string& error_message) -> Error { + // Note another_document does not exist at this point so set that and + // update document. + transaction.Update(document, data); + transaction.Set(another_document, data); + return Error::kErrorOk; + })); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +// TODO(zxu): There is no way to create Firestore with different project id yet. +TEST_F(ValidationTest, WritesMustNotContainReferencesToADifferentDatabase) {} + +TEST_F(ValidationTest, WritesMustNotContainReservedFieldNames) { + SCOPED_TRACE("WritesMustNotContainReservedFieldNames"); + + ExpectWriteError(MapFieldValue{{"__baz__", FieldValue::Integer(1)}}, + "Invalid data. Document fields cannot begin and end with " + "\"__\" (found in field __baz__)"); + ExpectWriteError( + MapFieldValue{ + {"foo", FieldValue::Map({{"__baz__", FieldValue::Integer(1)}})}}, + "Invalid data. Document fields cannot begin and end with \"__\" (found " + "in " + "field foo.__baz__)"); + ExpectWriteError( + MapFieldValue{ + {"__baz__", FieldValue::Map({{"foo", FieldValue::Integer(1)}})}}, + "Invalid data. Document fields cannot begin and end with \"__\" (found " + "in " + "field __baz__)"); + + ExpectUpdateError(MapFieldValue{{"__baz__", FieldValue::Integer(1)}}, + "Invalid data. Document fields cannot begin and end with " + "\"__\" (found in field __baz__)"); + ExpectUpdateError(MapFieldValue{{"baz.__foo__", FieldValue::Integer(1)}}, + "Invalid data. Document fields cannot begin and end with " + "\"__\" (found in field baz.__foo__)"); +} + +TEST_F(ValidationTest, SetsMustNotContainFieldValueDelete) { + SCOPED_TRACE("SetsMustNotContainFieldValueDelete"); + + ExpectSetError( + MapFieldValue{{"foo", FieldValue::Delete()}}, + "Invalid data. FieldValue.delete() can only be used with update() and " + "set() with SetOptions.merge() (found in field foo)"); +} + +TEST_F(ValidationTest, UpdatesMustNotContainNestedFieldValueDeletes) { + SCOPED_TRACE("UpdatesMustNotContainNestedFieldValueDeletes"); + + ExpectUpdateError( + MapFieldValue{{"foo", FieldValue::Map({{"bar", FieldValue::Delete()}})}}, + "Invalid data. FieldValue.delete() can only appear at the top level of " + "your update data (found in field foo.bar)"); +} + +TEST_F(ValidationTest, BatchWritesRequireCorrectDocumentReferences) { + DocumentReference bad_document = + CachedFirestore("another")->Document("foo/bar"); + + WriteBatch batch = firestore()->batch(); + try { + batch.Set(bad_document, MapFieldValue{{"foo", FieldValue::Integer(1)}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Provided document reference is from a different Cloud Firestore " + "instance.", + exception.what()); + } +} + +TEST_F(ValidationTest, TransactionsRequireCorrectDocumentReferences) {} + +TEST_F(ValidationTest, FieldPathsMustNotHaveEmptySegments) { + SCOPED_TRACE("FieldPathsMustNotHaveEmptySegments"); + + std::map bad_field_paths_and_errors = { + {"", + "Invalid field path (). Paths must not be empty, begin with '.', end " + "with '.', or contain '..'"}, + {"foo..baz", + "Invalid field path (foo..baz). Paths must not be empty, begin with " + "'.', end with '.', or contain '..'"}, + {".foo", + "Invalid field path (.foo). Paths must not be empty, begin with '.', " + "end with '.', or contain '..'"}, + {"foo.", + "Invalid field path (foo.). Paths must not be empty, begin with '.', " + "end with '.', or contain '..'"}}; + for (const auto path_and_error : bad_field_paths_and_errors) { + VerifyFieldPathThrows(path_and_error.first, path_and_error.second); + } +} + +TEST_F(ValidationTest, FieldPathsMustNotHaveInvalidSegments) { + SCOPED_TRACE("FieldPathsMustNotHaveInvalidSegments"); + + std::map bad_field_paths_and_errors = { + {"foo~bar", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo*bar", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo/bar", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo[1", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo]1", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo[1]", "Use FieldPath.of() for field names containing '~*/[]'."}, + }; + for (const auto path_and_error : bad_field_paths_and_errors) { + VerifyFieldPathThrows(path_and_error.first, path_and_error.second); + } +} + +TEST_F(ValidationTest, FieldNamesMustNotBeEmpty) { + DocumentSnapshot snapshot = ReadDocument(Document()); + // PORT_NOTE: We do not enforce any logic for invalid C++ object. In + // particular the creation of invalid object should be valid (for using + // standard container). We have not defined the behavior to call API with + // invalid object yet. + // try { + // snapshot.Get(FieldPath{}); + // FAIL() << "should throw exception"; + // } catch (const FirestoreException& exception) { + // EXPECT_STREQ("Invalid field path. Provided path must not be empty.", + // exception.what()); + // } + + try { + snapshot.Get(FieldPath{""}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid field name at argument 1. Field names must not be null or " + "empty.", + exception.what()); + } + try { + snapshot.Get(FieldPath{"foo", ""}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid field name at argument 2. Field names must not be null or " + "empty.", + exception.what()); + } +} + +TEST_F(ValidationTest, ArrayTransformsFailInQueries) { + CollectionReference collection = Collection(); + try { + collection.WhereEqualTo( + "test", + FieldValue::Map( + {{"test", FieldValue::ArrayUnion({FieldValue::Integer(1)})}})); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid data. FieldValue.arrayUnion() can only be used with set() and " + "update() (found in field test)", + exception.what()); + } + + try { + collection.WhereEqualTo( + "test", + FieldValue::Map( + {{"test", FieldValue::ArrayRemove({FieldValue::Integer(1)})}})); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid data. FieldValue.arrayRemove() can only be used with set() " + "and update() (found in field test)", + exception.what()); + } +} + +// PORT_NOTE: Does not apply to C++ which is strong-typed. +TEST_F(ValidationTest, ArrayTransformsRejectInvalidElements) {} + +TEST_F(ValidationTest, ArrayTransformsRejectArrays) { + DocumentReference document = Document(); + // This would result in a directly nested array which is not supported. + try { + document.Set(MapFieldValue{ + {"x", FieldValue::ArrayUnion( + {FieldValue::Integer(1), + FieldValue::Array({FieldValue::String("nested")})})}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ("Invalid data. Nested arrays are not supported", + exception.what()); + } + try { + document.Set(MapFieldValue{ + {"x", FieldValue::ArrayRemove( + {FieldValue::Integer(1), + FieldValue::Array({FieldValue::String("nested")})})}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ("Invalid data. Nested arrays are not supported", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithNonPositiveLimitFail) { + CollectionReference collection = Collection(); + try { + collection.Limit(0); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Query limit (0) is invalid. Limit must be positive.", + exception.what()); + } + try { + collection.Limit(-1); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Query limit (-1) is invalid. Limit must be positive.", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithNullOrNaNFiltersOtherThanEqualityFail) { + CollectionReference collection = Collection(); + try { + collection.WhereGreaterThan("a", FieldValue::Null()); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Null supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } + try { + collection.WhereArrayContains("a", FieldValue::Null()); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Null supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } + try { + collection.WhereGreaterThan("a", FieldValue::Double(NAN)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. NaN supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } + try { + collection.WhereArrayContains("a", FieldValue::Double(NAN)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. NaN supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesCannotBeCreatedFromDocumentsMissingSortValues) { + CollectionReference collection = + Collection(std::map{ + {"f", MapFieldValue{{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + + Query query = collection.OrderBy("sort"); + DocumentSnapshot snapshot = ReadDocument(collection.Document("f")); + + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}})); + const char* reason = + "Invalid query. You are trying to start or end a query using a document " + "for which the field 'sort' (used as the orderBy) does not exist."; + try { + query.StartAt(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.StartAfter(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.EndBefore(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.EndAt(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } +} + +TEST_F(ValidationTest, + QueriesCannotBeSortedByAnUncommittedServerTimestamp) { + CollectionReference collection = Collection(); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection); + + Await(firestore()->DisableNetwork()); + + Future future = collection.Document("doc").Set( + {{"timestamp", FieldValue::ServerTimestamp()}}); + + QuerySnapshot snapshot = accumulator.Await(); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + snapshot = accumulator.Await(); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + + EXPECT_THROW(collection.OrderBy(FieldPath({"timestamp"})) + .EndAt(snapshot.documents().at(0)) + .AddSnapshotListener([](const QuerySnapshot&, Error) {}), + FirestoreException); + + Await(firestore()->EnableNetwork()); + Await(future); + + snapshot = accumulator.Await(); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_NO_THROW(collection.OrderBy(FieldPath({"timestamp"})) + .EndAt(snapshot.documents().at(0)) + .AddSnapshotListener([](const QuerySnapshot&, Error) {})); +} + + +TEST_F(ValidationTest, QueriesMustNotHaveMoreComponentsThanOrderBy) { + CollectionReference collection = Collection(); + Query query = collection.OrderBy("foo"); + + const char* reason = + "Too many arguments provided to startAt(). The number of arguments must " + "be less than or equal to the number of orderBy() clauses."; + try { + query.StartAt({FieldValue::Integer(1), FieldValue::Integer(2)}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.OrderBy("bar").StartAt({FieldValue::Integer(1), + FieldValue::Integer(2), + FieldValue::Integer(3)}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } +} + +TEST_F(ValidationTest, QueryOrderByKeyBoundsMustBeStringsWithoutSlashes) { + CollectionReference collection = Collection(); + Query query = collection.OrderBy(FieldPath::DocumentId()); + try { + query.StartAt({FieldValue::Integer(1)}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. Expected a string for document ID in startAt(), but " + "got 1.", + exception.what()); + } + try { + query.StartAt({FieldValue::String("foo/bar")}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying a collection and ordering by " + "FieldPath.documentId(), the value passed to startAt() must be a plain " + "document ID, but 'foo/bar' contains a slash.", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithDifferentInequalityFieldsFail) { + try { + Collection() + .WhereGreaterThan("x", FieldValue::Integer(32)) + .WhereLessThan("y", FieldValue::String("cat")); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "All where filters other than whereEqualTo() must be on the same " + "field. But you have filters on 'x' and 'y'", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithInequalityDifferentThanFirstOrderByFail) { + CollectionReference collection = Collection(); + const char* reason = + "Invalid query. You have an inequality where filter (whereLessThan(), " + "whereGreaterThan(), etc.) on field 'x' and so you must also have 'x' as " + "your first orderBy() field, but your first orderBy() is currently on " + "field 'y' instead."; + try { + collection.WhereGreaterThan("x", FieldValue::Integer(32)).OrderBy("y"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + collection.OrderBy("y").WhereGreaterThan("x", FieldValue::Integer(32)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + collection.WhereGreaterThan("x", FieldValue::Integer(32)) + .OrderBy("y") + .OrderBy("x"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + collection.OrderBy("y").OrderBy("x").WhereGreaterThan( + "x", FieldValue::Integer(32)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithMultipleArrayContainsFiltersFail) { + try { + Collection() + .WhereArrayContains("foo", FieldValue::Integer(1)) + .WhereArrayContains("foo", FieldValue::Integer(2)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. You cannot use more than one 'array_contains' filter.", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesMustNotSpecifyStartingOrEndingPointAfterOrderBy) { + CollectionReference collection = Collection(); + Query query = collection.OrderBy("foo"); + try { + query.StartAt({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.startAt() or " + "Query.startAfter() before calling Query.orderBy().", + exception.what()); + } + try { + query.StartAfter({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.startAt() or " + "Query.startAfter() before calling Query.orderBy().", + exception.what()); + } + try { + query.EndAt({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.endAt() or " + "Query.endBefore() before calling Query.orderBy().", + exception.what()); + } + try { + query.EndBefore({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.endAt() or " + "Query.endBefore() before calling Query.orderBy().", + exception.what()); + } +} + +TEST_F(ValidationTest, + QueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences) { + CollectionReference collection = Collection(); + try { + collection.WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("")); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying with FieldPath.documentId() you must " + "provide a valid document ID, but it was an empty string.", + exception.what()); + } + + try { + collection.WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("foo/bar/baz")); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying a collection by FieldPath.documentId() " + "you must provide a plain document ID, but 'foo/bar/baz' contains a " + "'/' character.", + exception.what()); + } + + try { + collection.WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::Integer(1)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying with FieldPath.documentId() you must " + "provide a valid String or DocumentReference, but it was of type: " + "java.lang.Long", + exception.what()); + } + + try { + collection.WhereArrayContains(FieldPath::DocumentId(), + FieldValue::Integer(1)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You can't perform 'array_contains' queries on " + "FieldPath.documentId().", + exception.what()); + } +} + +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/write_batch_test.cc b/firestore/src/tests/write_batch_test.cc new file mode 100644 index 0000000000..69c7cf8abf --- /dev/null +++ b/firestore/src/tests/write_batch_test.cc @@ -0,0 +1,314 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/write_batch_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/WriteBatchTest.java +// The test cases between the two native client SDK divert quite a lot. The port +// here is an effort to do a superset and cover both cases. + +namespace firebase { +namespace firestore { + +using WriteBatchCommonTest = testing::Test; + +using WriteBatchTest = FirestoreIntegrationTest; + +TEST_F(WriteBatchTest, TestSupportEmptyBatches) { + Await(firestore()->batch().Commit()); +} + +TEST_F(WriteBatchTest, TestSetDocuments) { + DocumentReference doc = Document(); + Await(firestore() + ->batch() + .Set(doc, MapFieldValue{{"a", FieldValue::String("b")}}) + .Set(doc, MapFieldValue{{"c", FieldValue::String("d")}}) + .Set(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +} + +TEST_F(WriteBatchTest, TestSetDocumentWithMerge) { + DocumentReference doc = Document(); + Await(firestore() + ->batch() + .Set(doc, + MapFieldValue{ + {"a", FieldValue::String("b")}, + {"nested", + FieldValue::Map({{"a", FieldValue::String("remove")}})}}, + SetOptions::Merge()) + .Commit()); + Await(firestore() + ->batch() + .Set(doc, + MapFieldValue{ + {"c", FieldValue::String("d")}, + {"ignore", FieldValue::Boolean(true)}, + {"nested", + FieldValue::Map({{"c", FieldValue::String("d")}})}}, + SetOptions::MergeFields({"c", "nested"})) + .Commit()); + Await(firestore() + ->batch() + .Set(doc, + MapFieldValue{ + {"e", FieldValue::String("f")}, + {"nested", FieldValue::Map( + {{"e", FieldValue::String("f")}, + {"ignore", FieldValue::Boolean(true)}})}}, + SetOptions::MergeFieldPaths({{"e"}, {"nested", "e"}})) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("b")}, + {"c", FieldValue::String("d")}, + {"e", FieldValue::String("f")}, + {"nested", FieldValue::Map({{"c", FieldValue::String("d")}, + {"e", FieldValue::String("f")}})}})); +} + +TEST_F(WriteBatchTest, TestUpdateDocuments) { + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + Await(firestore() + ->batch() + .Update(doc, MapFieldValue{{"baz", FieldValue::Integer(42)}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"foo", FieldValue::String("bar")}, + {"baz", FieldValue::Integer(42)}})); +} + +TEST_F(WriteBatchTest, TestCannotUpdateNonexistentDocuments) { + DocumentReference doc = Document(); + Await(firestore() + ->batch() + .Update(doc, MapFieldValue{{"baz", FieldValue::Integer(42)}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(WriteBatchTest, TestDeleteDocuments) { + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + DocumentSnapshot snapshot = ReadDocument(doc); + + EXPECT_TRUE(snapshot.exists()); + Await(firestore()->batch().Delete(doc).Commit()); + snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(WriteBatchTest, TestBatchesCommitAtomicallyRaisingCorrectEvents) { + CollectionReference collection = Collection(); + DocumentReference doc_a = collection.Document("a"); + DocumentReference doc_b = collection.Document("b"); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + QuerySnapshot initial_snapshot = accumulator.Await(); + EXPECT_EQ(0, initial_snapshot.size()); + + // Atomically write two documents. + Await(firestore() + ->batch() + .Set(doc_a, MapFieldValue{{"a", FieldValue::Integer(1)}}) + .Set(doc_b, MapFieldValue{{"b", FieldValue::Integer(2)}}) + .Commit()); + + QuerySnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(local_snapshot), + testing::ElementsAre(MapFieldValue{{"a", FieldValue::Integer(1)}}, + MapFieldValue{{"b", FieldValue::Integer(2)}})); + + QuerySnapshot server_snapshot = accumulator.Await(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(server_snapshot), + testing::ElementsAre(MapFieldValue{{"a", FieldValue::Integer(1)}}, + MapFieldValue{{"b", FieldValue::Integer(2)}})); +} + +TEST_F(WriteBatchTest, TestBatchesFailAtomicallyRaisingCorrectEvents) { + CollectionReference collection = Collection(); + DocumentReference doc_a = collection.Document("a"); + DocumentReference doc_b = collection.Document("b"); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + QuerySnapshot initial_snapshot = accumulator.Await(); + EXPECT_EQ(0, initial_snapshot.size()); + + // Atomically write 1 document and update a nonexistent document. + Future future = + firestore() + ->batch() + .Set(doc_a, MapFieldValue{{"a", FieldValue::Integer(1)}}) + .Update(doc_b, MapFieldValue{{"b", FieldValue::Integer(2)}}) + .Commit(); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); + + // Local event with the set document. + QuerySnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(local_snapshot), + testing::ElementsAre(MapFieldValue{{"a", FieldValue::Integer(1)}})); + + // Server event with the set reverted + QuerySnapshot server_snapshot = accumulator.Await(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + EXPECT_EQ(0, server_snapshot.size()); +} + +TEST_F(WriteBatchTest, TestWriteTheSameServerTimestampAcrossWrites) { + CollectionReference collection = Collection(); + DocumentReference doc_a = collection.Document("a"); + DocumentReference doc_b = collection.Document("b"); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + QuerySnapshot initial_snapshot = accumulator.Await(); + EXPECT_EQ(0, initial_snapshot.size()); + + // Atomically write two documents with server timestamps. + Await(firestore() + ->batch() + .Set(doc_a, MapFieldValue{{"when", FieldValue::ServerTimestamp()}}) + .Set(doc_b, MapFieldValue{{"when", FieldValue::ServerTimestamp()}}) + .Commit()); + + QuerySnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(local_snapshot), + testing::ElementsAre(MapFieldValue{{"when", FieldValue::Null()}}, + MapFieldValue{{"when", FieldValue::Null()}})); + + QuerySnapshot server_snapshot = accumulator.AwaitRemoteEvent(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + EXPECT_EQ(2, server_snapshot.size()); + const FieldValue when = server_snapshot.documents()[0].Get("when"); + EXPECT_EQ(FieldValue::Type::kTimestamp, when.type()); + EXPECT_THAT(QuerySnapshotToValues(server_snapshot), + testing::ElementsAre(MapFieldValue{{"when", when}}, + MapFieldValue{{"when", when}})); +} + +TEST_F(WriteBatchTest, TestCanWriteTheSameDocumentMultipleTimes) { + DocumentReference doc = Document(); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&doc, MetadataChanges::kInclude); + DocumentSnapshot initial_snapshot = accumulator.Await(); + EXPECT_FALSE(initial_snapshot.exists()); + + Await(firestore() + ->batch() + .Delete(doc) + .Set(doc, MapFieldValue{{"a", FieldValue::Integer(1)}, + {"b", FieldValue::Integer(1)}, + {"when", FieldValue::String("when")}}) + .Update(doc, MapFieldValue{{"b", FieldValue::Integer(2)}, + {"when", FieldValue::ServerTimestamp()}}) + .Commit()); + DocumentSnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT(local_snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Integer(1)}, + {"b", FieldValue::Integer(2)}, + {"when", FieldValue::Null()}})); + + DocumentSnapshot server_snapshot = accumulator.Await(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + const FieldValue when = server_snapshot.Get("when"); + EXPECT_EQ(FieldValue::Type::kTimestamp, when.type()); + EXPECT_THAT(server_snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Integer(1)}, + {"b", FieldValue::Integer(2)}, + {"when", when}})); +} + +TEST_F(WriteBatchTest, TestUpdateFieldsWithDots) { + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"a.b", FieldValue::String("old")}, + {"c.d", FieldValue::String("old")}}); + Await(firestore() + ->batch() + .Update(doc, MapFieldPathValue{{FieldPath{"a.b"}, + FieldValue::String("new")}}) + .Commit()); + Await(firestore() + ->batch() + .Update(doc, MapFieldPathValue{{FieldPath{"c.d"}, + FieldValue::String("new")}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a.b", FieldValue::String("new")}, + {"c.d", FieldValue::String("new")}})); +} + +TEST_F(WriteBatchTest, TestUpdateNestedFields) { + DocumentReference doc = Document(); + WriteDocument( + doc, MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("old")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("old")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}}); + Await(firestore() + ->batch() + .Update(doc, MapFieldValue({{"a.b", FieldValue::String("new")}})) + .Commit()); + Await(firestore() + ->batch() + .Update(doc, MapFieldPathValue({{FieldPath{"c", "d"}, + FieldValue::String("new")}})) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("new")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("new")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); +} + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +TEST_F(WriteBatchCommonTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(WriteBatchCommonTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/instance_id/src_ios/fake/FIRInstanceID.h b/instance_id/src_ios/fake/FIRInstanceID.h new file mode 100644 index 0000000000..ced17b5891 --- /dev/null +++ b/instance_id/src_ios/fake/FIRInstanceID.h @@ -0,0 +1,330 @@ +// Copyright 2017 Google LLC +// +// 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. + +#ifndef FIREBASE_INSTANCE_ID_CLIENT_CPP_SRC_IOS_FAKE_H_ +#define FIREBASE_INSTANCE_ID_CLIENT_CPP_SRC_IOS_FAKE_H_ + +#ifdef __OBJC__ +#import +#endif // __OBJC__ + +// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. +// Wrap it in our own macro if it's a non-compatible SDK. +#ifndef FIR_SWIFT_NAME +#ifdef __IPHONE_9_3 +#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) +#else +#define FIR_SWIFT_NAME(X) // Intentionally blank. +#endif // #ifdef __IPHONE_9_3 +#endif // #ifndef FIR_SWIFT_NAME + +// C++ enumeration used to inject FIRInstanceIDError values from a C++ test. +enum FIRInstanceIDErrorCode { + kFIRInstanceIDErrorCodeNone = -1, + kFIRInstanceIDErrorCodeUnknown = 0, + kFIRInstanceIDErrorCodeAuthentication = 1, + kFIRInstanceIDErrorCodeNoAccess = 2, + kFIRInstanceIDErrorCodeTimeout = 3, + kFIRInstanceIDErrorCodeNetwork = 4, + kFIRInstanceIDErrorCodeOperationInProgress = 5, + kFIRInstanceIDErrorCodeInvalidRequest = 7, +}; + +// Initialize the mock module. +void FIRInstanceIDInitialize(); + +// Set the next error to be raised by the mock. +void FIRInstanceIDSetNextErrorCode(FIRInstanceIDErrorCode errorCode); + +// Enable / disable blocking on an asynchronous operation. +bool FIRInstanceIDSetBlockingMethodCallsEnable(bool enable); + +// Wait for an operation to start. +bool FIRInstanceIDWaitForBlockedThread(); + +#ifdef __OBJC__ +/** + * @memberof FIRInstanceID + * + * The scope to be used when fetching/deleting a token for Firebase Messaging. + */ +FOUNDATION_EXPORT NSString * __nonnull const kFIRInstanceIDScopeFirebaseMessaging + FIR_SWIFT_NAME(InstanceIDScopeFirebaseMessaging); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + * Called when the system determines that tokens need to be refreshed. + * This method is also called if Instance ID has been reset in which + * case, tokens and FCM topic subscriptions also need to be refreshed. + * + * Instance ID service will throttle the refresh event across all devices + * to control the rate of token updates on application servers. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull kFIRInstanceIDTokenRefreshNotification + FIR_SWIFT_NAME(InstanceIDTokenRefresh); +#else +/** + * Called when the system determines that tokens need to be refreshed. + * This method is also called if Instance ID has been reset in which + * case, tokens and FCM topic subscriptions also need to be refreshed. + * + * Instance ID service will throttle the refresh event across all devices + * to control the rate of token updates on application servers. + */ +FOUNDATION_EXPORT NSString * __nonnull const kFIRInstanceIDTokenRefreshNotification + FIR_SWIFT_NAME(InstanceIDTokenRefreshNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the InstanceID token returns. If + * the call fails we return the appropriate `error code` as described below. + * + * @param token The valid token as returned by InstanceID backend. + * + * @param error The error describing why generating a new token + * failed. See the error codes below for a more detailed + * description. + */ +typedef void(^FIRInstanceIDTokenHandler)( NSString * __nullable token, NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDTokenHandler); + + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the InstanceID `deleteToken` returns. If + * the call fails we return the appropriate `error code` as described below + * + * @param error The error describing why deleting the token failed. + * See the error codes below for a more detailed description. + */ +typedef void(^FIRInstanceIDDeleteTokenHandler)(NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDDeleteTokenHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity is created. If the + * identity wasn't created for some reason we return the appropriate error code. + * + * @param identity A valid identity for the app instance, nil if there was an error + * while creating an identity. + * @param error The error if fetching the identity fails else nil. + */ +typedef void(^FIRInstanceIDHandler)(NSString * __nullable identity, NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity and all the tokens associated + * with it are deleted. Returns a valid error object in case of failure else nil. + * + * @param error The error if deleting the identity and all the tokens associated with + * it fails else nil. + */ +typedef void(^FIRInstanceIDDeleteHandler)(NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDDeleteHandler); + +/** + * Public errors produced by InstanceID. + */ +typedef NS_ENUM(NSUInteger, FIRInstanceIDError) { + // Http related errors. + + /// Unknown error. + FIRInstanceIDErrorUnknown = 0, + + /// Auth Error -- GCM couldn't validate request from this client. + FIRInstanceIDErrorAuthentication = 1, + + /// NoAccess -- InstanceID service cannot be accessed. + FIRInstanceIDErrorNoAccess = 2, + + /// Timeout -- Request to InstanceID backend timed out. + FIRInstanceIDErrorTimeout = 3, + + /// Network -- No network available to reach the servers. + FIRInstanceIDErrorNetwork = 4, + + /// OperationInProgress -- Another similar operation in progress, + /// bailing this one. + FIRInstanceIDErrorOperationInProgress = 5, + + /// InvalidRequest -- Some parameters of the request were invalid. + FIRInstanceIDErrorInvalidRequest = 7, +} FIR_SWIFT_NAME(InstanceIDError); + +static_assert(static_cast(FIRInstanceIDErrorUnknown) == + static_cast(kFIRInstanceIDErrorCodeUnknown), ""); +static_assert(static_cast(FIRInstanceIDErrorAuthentication) == + static_cast(kFIRInstanceIDErrorCodeAuthentication), ""); +static_assert(static_cast(FIRInstanceIDErrorNoAccess) == + static_cast(kFIRInstanceIDErrorCodeNoAccess), ""); +static_assert(static_cast(FIRInstanceIDErrorTimeout) == + static_cast(kFIRInstanceIDErrorCodeTimeout), ""); +static_assert(static_cast(FIRInstanceIDErrorNetwork) == + static_cast(kFIRInstanceIDErrorCodeNetwork), ""); +static_assert(static_cast(FIRInstanceIDErrorOperationInProgress) == + static_cast(kFIRInstanceIDErrorCodeOperationInProgress), ""); +static_assert(static_cast(FIRInstanceIDErrorInvalidRequest) == + static_cast(kFIRInstanceIDErrorCodeInvalidRequest), ""); + +/** + * The APNS token type for the app. If the token type is set to `UNKNOWN` + * InstanceID will implicitly try to figure out what the actual token type + * is from the provisioning profile. + */ +typedef NS_ENUM(NSInteger, FIRInstanceIDAPNSTokenType) { + /// Unknown token type. + FIRInstanceIDAPNSTokenTypeUnknown, + /// Sandbox token type. + FIRInstanceIDAPNSTokenTypeSandbox, + /// Production token type. + FIRInstanceIDAPNSTokenTypeProd, +} FIR_SWIFT_NAME(InstanceIDAPNSTokenType) + __deprecated_enum_msg("Use FIRMessaging's APNSToken property instead."); + +/** + * Instance ID provides a unique identifier for each app instance and a mechanism + * to authenticate and authorize actions (for example, sending an FCM message). + * + * Instance ID is long lived but, may be reset if the device is not used for + * a long time or the Instance ID service detects a problem. + * If Instance ID is reset, the app will be notified via + * `kFIRInstanceIDTokenRefreshNotification`. + * + * If the Instance ID has become invalid, the app can request a new one and + * send it to the app server. + * To prove ownership of Instance ID and to allow servers to access data or + * services associated with the app, call + * `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. + */ +FIR_SWIFT_NAME(InstanceID) +@interface FIRInstanceID : NSObject + +/** + * FIRInstanceID. + * + * @return A shared instance of FIRInstanceID. + */ ++ (nonnull instancetype)instanceID FIR_SWIFT_NAME(instanceID()); + +#pragma mark - Tokens + +/** + * Returns a Firebase Messaging scoped token for the firebase app. + * + * @return Null Returns null if the device has not yet been registerd with + * Firebase Message else returns a valid token. + */ +- (nullable NSString *)token; + +/** + * Returns a token that authorizes an Entity (example: cloud service) to perform + * an action on behalf of the application identified by Instance ID. + * + * This is similar to an OAuth2 token except, it applies to the + * application instance instead of a user. + * + * This is an asynchronous call. If the token fetching fails for some reason + * we invoke the completion callback with nil `token` and the appropriate + * error. + * + * Note, you can only have one `token` or `deleteToken` call for a given + * authorizedEntity and scope at any point of time. Making another such call with the + * same authorizedEntity and scope before the last one finishes will result in an + * error with code `OperationInProgress`. + * + * @see FIRInstanceID deleteTokenWithAuthorizedEntity:scope:handler: + * + * @param authorizedEntity Entity authorized by the token. + * @param scope Action authorized for authorizedEntity. + * @param options The extra options to be sent with your token request. The + * value for the `apns_token` should be the NSData object + * passed to the UIApplicationDelegate's + * `didRegisterForRemoteNotificationsWithDeviceToken` method. + * The value for `apns_sandbox` should be a boolean (or an + * NSNumber representing a BOOL in Objective C) set to true if + * your app is a debug build, which means that the APNs + * device token is for the sandbox environment. It should be + * set to false otherwise. If the `apns_sandbox` key is not + * provided, an automatically-detected value shall be used. + * @param handler The callback handler which is invoked when the token is + * successfully fetched. In case of success a valid `token` and + * `nil` error are returned. In case of any error the `token` + * is nil and a valid `error` is returned. The valid error + * codes have been documented above. + */ +- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + options:(nullable NSDictionary *)options + handler:(nonnull FIRInstanceIDTokenHandler)handler; + +/** + * Revokes access to a scope (action) for an entity previously + * authorized by `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. + * + * This is an asynchronous call. Call this on the main thread since InstanceID lib + * is not thread safe. In case token deletion fails for some reason we invoke the + * `handler` callback passed in with the appropriate error code. + * + * Note, you can only have one `token` or `deleteToken` call for a given + * authorizedEntity and scope at a point of time. Making another such call with the + * same authorizedEntity and scope before the last one finishes will result in an error + * with code `OperationInProgress`. + * + * @param authorizedEntity Entity that must no longer have access. + * @param scope Action that entity is no longer authorized to perform. + * @param handler The handler that is invoked once the unsubscribe call ends. + * In case of error an appropriate error object is returned + * else error is nil. + */ +- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + handler:(nonnull FIRInstanceIDDeleteTokenHandler)handler; + +#pragma mark - Identity + +/** + * Asynchronously fetch a stable identifier that uniquely identifies the app + * instance. If the identifier has been revoked or has expired, this method will + * return a new identifier. + * + * + * @param handler The handler to invoke once the identifier has been fetched. + * In case of error an appropriate error object is returned else + * a valid identifier is returned and a valid identifier for the + * application instance. + */ +- (void)getIDWithHandler:(nonnull FIRInstanceIDHandler)handler + FIR_SWIFT_NAME(getID(handler:)); + +/** + * Resets Instance ID and revokes all tokens. + * + * This method also triggers a request to fetch a new Instance ID and Firebase Messaging scope + * token. Please listen to kFIRInstanceIDTokenRefreshNotification when the new ID and token are + * ready. + */ +- (void)deleteIDWithHandler:(nonnull FIRInstanceIDDeleteHandler)handler + FIR_SWIFT_NAME(deleteID(handler:)); + +@end + +#endif // __OBJC__ + +#endif // FIREBASE_INSTANCE_ID_CLIENT_CPP_SRC_IOS_FAKE_H_ diff --git a/instance_id/src_ios/fake/FIRInstanceID.mm b/instance_id/src_ios/fake/FIRInstanceID.mm new file mode 100644 index 0000000000..539ae0198c --- /dev/null +++ b/instance_id/src_ios/fake/FIRInstanceID.mm @@ -0,0 +1,158 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "instance_id/src_ios/fake/FIRInstanceID.h" + +#include + +#include + +#import + +#include "testing/reporter_impl.h" + +static FIRInstanceIDErrorCode gNextErrorCode = kFIRInstanceIDErrorCodeNone; +static bool gBlockingEnabled = false; + +static dispatch_semaphore_t gBlocking; +static dispatch_semaphore_t gThreadStarted; +static dispatch_semaphore_t gThreadComplete; + +// Initialize the mock module. +void FIRInstanceIDInitialize() { + gBlocking = dispatch_semaphore_create(0); + gThreadStarted = dispatch_semaphore_create(0); + gThreadComplete = dispatch_semaphore_create(0); + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeNone); +} + +// Set the next error to be raised by the mock. +void FIRInstanceIDSetNextErrorCode(FIRInstanceIDErrorCode errorCode) { + gNextErrorCode = errorCode; +} + +// Retrieve the next error code and clear the current error code. +static FIRInstanceIDErrorCode GetAndClearErrorCode() { + FIRInstanceIDErrorCode errorCode = gNextErrorCode; + gNextErrorCode = kFIRInstanceIDErrorCodeNone; + return errorCode; +} + +// Wait 1 second while trying to acquire a semaphore, returning false on timeout. +static bool WaitForSemaphore(dispatch_semaphore_t semaphore) { + static const int64_t kSemaphoreWaitTimeoutNanoseconds = 1000000000 /* 1s */; + return dispatch_semaphore_wait(semaphore, + dispatch_time(DISPATCH_TIME_NOW, + kSemaphoreWaitTimeoutNanoseconds)) == 0; +} + +// Enable / disable blocking on an asynchronous operation. +bool FIRInstanceIDSetBlockingMethodCallsEnable(bool enable) { + bool stateChanged = gBlockingEnabled != enable; + if (stateChanged) { + if (enable) { + gBlockingEnabled = enable; + } else { + gBlockingEnabled = enable; + dispatch_semaphore_signal(gBlocking); + if (!WaitForSemaphore(gThreadComplete)) return false; + } + } + return true; +} + +// Wait for an operation to start. +bool FIRInstanceIDWaitForBlockedThread() { + return WaitForSemaphore(gThreadStarted); +} + +// Run a block on a background thread. +static void RunBlockOnBackgroundThread(void (^block)(NSError* _Nullable error)) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_semaphore_signal(gThreadStarted); + int error_code = GetAndClearErrorCode(); + NSError * _Nullable error = nil; + if (error_code != kFIRInstanceIDErrorCodeNone) { + NSDictionary* userInfo = @{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Mock error code %d", error_code] + }; + error = [NSError errorWithDomain:@"Mock error" + code:error_code + userInfo:userInfo]; + } + else if (gBlockingEnabled) { + if (!WaitForSemaphore(gBlocking)) { + error = [NSError errorWithDomain:@"Timeout" + code:-1 + userInfo:nil]; + } + } + block(error); + dispatch_semaphore_signal(gThreadComplete); + }); +} + +@implementation FIRInstanceID + ++ (instancetype)instanceID { + if (GetAndClearErrorCode() != kFIRInstanceIDErrorCodeNone) return nil; + FakeReporter->AddReport("FirebaseInstanceId.construct", {}); + return [[FIRInstanceID alloc] init]; +} + +- (NSString*)token { + FakeReporter->AddReport("FirebaseInstanceId.getToken", {}); + return @"FakeToken"; +} + +- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + options:(nullable NSDictionary *)options + handler:(nonnull FIRInstanceIDTokenHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) { + FakeReporter->AddReport("FirebaseInstanceId.getToken", + { authorizedEntity.UTF8String, scope.UTF8String }); + } + handler(error ? nil : @"FakeToken", error); + }); +} + +- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + handler:(nonnull FIRInstanceIDDeleteTokenHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) { + FakeReporter->AddReport("FirebaseInstanceId.deleteToken", + { authorizedEntity.UTF8String, scope.UTF8String }); + } + handler(error); + }); +} + +- (void)getIDWithHandler:(nonnull FIRInstanceIDHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) FakeReporter->AddReport("FirebaseInstanceId.getId", {}); + handler(error ? nil : @"FakeId", error); + }); +} + +- (void)deleteIDWithHandler:(nonnull FIRInstanceIDDeleteHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) FakeReporter->AddReport("FirebaseInstanceId.deleteId", {}); + handler(error); + }); +} + +@end diff --git a/instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java b/instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java new file mode 100644 index 0000000000..2715dd0b84 --- /dev/null +++ b/instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java @@ -0,0 +1,187 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.iid; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.testing.cppsdk.FakeReporter; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.GuardedBy; + +/** + * Mock FirebaseInstanceId. + */ +public class FirebaseInstanceId { + + /** + * If set, {@link throwExceptionOrBlockThreadIfEnabled} will throw an IOException or + * IllegalStateException (from the constructor) with this message. + */ + private static String exceptionErrorMessage = null; + + /** If set, all method calls will block until this is signalled. */ + @GuardedBy("threadStarted") + private static CountDownLatch threadBlocker = null; + + /** Sempahore used to wait for a thread to start. */ + private static final Semaphore threadStarted = new Semaphore(0); + + /** Semaphore used to wait for the woken up thread to finish. */ + private static final Semaphore threadFinished = new Semaphore(0); + + /** + * Set a message which will be used to throw an Exception from all method calls. + * Clear the message by setting the value to null. + */ + public static void setThrowExceptionMessage(String errorMessage) { + exceptionErrorMessage = errorMessage; + } + + /** Make all operations block indefinitely until this flag is cleared. */ + public static boolean setBlockingMethodCallsEnable(boolean enable) { + boolean stateChanged = false; + synchronized (threadStarted) { + if ((enable && threadBlocker == null) || (!enable && threadBlocker != null)) { + stateChanged = true; + } + if (enable && stateChanged) { + threadBlocker = new CountDownLatch(1); + threadStarted.drainPermits(); + threadFinished.drainPermits(); + } + } + if (stateChanged && !enable) { + synchronized (threadStarted) { + threadBlocker.countDown(); + threadBlocker = null; + } + try { + boolean acquired = threadFinished.tryAcquire(1, 1, TimeUnit.SECONDS); + if (!acquired) { + return false; + } + } catch (InterruptedException e) { + return false; + } + } + return true; + } + + /** Wait for a thread to start and wait on {@link threadBlocker}. */ + public static boolean waitForBlockedThread() { + boolean acquired = false; + try { + acquired = threadStarted.tryAcquire(1, 1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + return acquired; + } + + /** Block the thread if enabled by {@link setBlockingMethodCallsEnable}. */ + private static void blockThreadIfEnabled() { + threadStarted.release(); + try { + CountDownLatch latch = null; + synchronized (threadStarted) { + latch = threadBlocker; + } + if (latch != null) { + latch.await(); + } + } catch (InterruptedException e) { + return; + } + } + + /** Signal thread completion to continue execution in {@link setBlockingMethodCallsEnable}. */ + private static void signalThreadCompletion() { + threadFinished.release(); + } + + /** + * Throw an exception or block the thread if enabled by {@link setThrowExceptionMessage} or + * {@link setBlockingMethodCallsEnabled} respectively. + */ + private static void throwExceptionOrBlockThreadIfEnabled() throws IOException { + if (exceptionErrorMessage != null) { + throw new IOException(exceptionErrorMessage); + } + blockThreadIfEnabled(); + } + + // Fake interface below. + + private FirebaseInstanceId() { + if (exceptionErrorMessage != null) { + throw new IllegalStateException(exceptionErrorMessage); + } + FakeReporter.addReport("FirebaseInstanceId.construct"); + } + + public String getId() { + try { + blockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.getId"); + } finally { + signalThreadCompletion(); + } + return "FakeId"; + } + + public long getCreationTime() { + try { + blockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.getCreationTime"); + } finally { + signalThreadCompletion(); + } + return 1512000287000L; // 11/29/17 16:04:47 + } + + public void deleteInstanceId() throws IOException { + try { + throwExceptionOrBlockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.deleteId"); + } finally { + signalThreadCompletion(); + } + } + + public String getToken(String authorizedEntity, String scope) throws IOException { + try { + throwExceptionOrBlockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.getToken", authorizedEntity, scope); + } finally { + signalThreadCompletion(); + } + return "FakeToken"; + } + + public void deleteToken(String authorizedEntity, String scope) throws IOException { + try { + throwExceptionOrBlockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.deleteToken", authorizedEntity, scope); + } finally { + signalThreadCompletion(); + } + } + + public static synchronized FirebaseInstanceId getInstance(FirebaseApp app) { + return new FirebaseInstanceId(); + } +} diff --git a/instance_id/tests/CMakeLists.txt b/instance_id/tests/CMakeLists.txt new file mode 100644 index 0000000000..3caf5d5944 --- /dev/null +++ b/instance_id/tests/CMakeLists.txt @@ -0,0 +1,36 @@ +# Copyright 2019 Google LLC +# +# 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. + +firebase_cpp_cc_test( + firebase_instance_id_test + SOURCES + ${FIREBASE_SOURCE_DIR}/instance_id/tests/instance_id_test.cc + DEPENDS + firebase_app_for_testing + firebase_instance_id + firebase_testing +) + +firebase_cpp_cc_test_on_ios( + firebase_instance_id_test + HOST + firebase_app_for_testing_ios + SOURCES + ${FIREBASE_SOURCE_DIR}/instance_id/tests/instance_id_test.cc + ${FIREBASE_SOURCE_DIR}/instance_id/src_ios/fake/FIRInstanceID.mm + DEPENDS + firebase_instance_id + firebase_testing +) + diff --git a/instance_id/tests/instance_id_test.cc b/instance_id/tests/instance_id_test.cc new file mode 100644 index 0000000000..a4aea63d1e --- /dev/null +++ b/instance_id/tests/instance_id_test.cc @@ -0,0 +1,546 @@ +// Copyright 2017 Google LLC +// +// 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. + +// WARNING: Some Code from this file is included verbatim in the C++ +// documentation. Only change existing code if it is safe to release +// to the public. Otherwise, a tech writer may make an unrelated +// modification, regenerate the docs, and unwittingly release an +// unannounced modification to the public. + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#include +#define __ANDROID__ +#include + +#include + +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#if defined(__APPLE__) +#include "TargetConditionals.h" +#endif // defined(__APPLE__) + +// [START instance_id_includes] +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "app/src/log.h" +#include "app/src/time.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "instance_id/src/include/firebase/instance_id.h" +// [END instance_id_includes] +#if TARGET_OS_IPHONE +#include "instance_id/src_ios/fake/FIRInstanceID.h" +#endif // TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" + +using testing::Eq; +using testing::IsNull; +using testing::MatchesRegex; +using testing::NotNull; + +namespace firebase { +namespace instance_id { + +class InstanceIdTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + AppOptions options; + options.set_messaging_sender_id("123456"); +#if TARGET_OS_IPHONE + FIRInstanceIDInitialize(); +#endif // TARGET_OS_IPHONE + reporter_.reset(); + app_ = testing::CreateApp(); + } + + void TearDown() override { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage(nullptr); + SetBlockingMethodCallsEnable(false); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + firebase::testing::cppsdk::ConfigReset(); + delete app_; + app_ = nullptr; + EXPECT_THAT(reporter_.getFakeReports(), Eq(reporter_.getExpectations())); + } + + void AddExpectationAndroid(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kAndroid, + args); + } + + void AddExpectationIos(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kIos, args); + } + + void AddExpectationAndroidIos(const char* fake, + std::initializer_list args) { + AddExpectationAndroid(fake, args); + AddExpectationIos(fake, args); + } + + // Wait for a future up to the specified number of milliseconds. + template + static void WaitForFutureWithTimeout( + const Future& future, + int timeout_milliseconds = kFutureTimeoutMilliseconds, + FutureStatus expected_status = kFutureStatusComplete) { + while (future.status() != expected_status && timeout_milliseconds-- > 0) { + ::firebase::internal::Sleep(1); + } + } + + // Validate that a future completed successfully and has the specified + // result. + template + static void CheckSuccessWithValue(const Future& future, const T& result) { + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.error(), Eq(instance_id::kErrorNone)); + EXPECT_THAT(*future.result(), Eq(result)); + } + + // Validate that a future completed successfully. + static void CheckSuccess(const Future& future) { + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.error(), Eq(instance_id::kErrorNone)); + } + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Find the mock FirebaseInstanceId class. + static void GetMockClass( + const std::function& retrieved_class) { + JNIEnv* env = firebase::testing::cppsdk::GetTestJniEnv(); + jclass clazz = env->FindClass("com/google/firebase/iid/FirebaseInstanceId"); + retrieved_class(env, clazz); + env->DeleteLocalRef(clazz); + } +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Set the exception message to throw from the next method call to the fake. + static void SetThrowExceptionMessage(const char* message) { + GetMockClass([&message](JNIEnv* env, jclass clazz) { + jmethodID methodId = env->GetStaticMethodID( + clazz, "setThrowExceptionMessage", "(Ljava/lang/String;)V"); + jobject stringobj = message ? env->NewStringUTF(message) : nullptr; + env->CallStaticVoidMethod(clazz, methodId, stringobj); + if (env->ExceptionCheck()) env->ExceptionClear(); + if (stringobj) env->DeleteLocalRef(stringobj); + }); + } +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + + // Enable / disable indefinite blocking of all mock method calls. + static bool SetBlockingMethodCallsEnable(bool enable) { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + bool successful = false; + GetMockClass([&enable, &successful](JNIEnv* env, jclass clazz) { + jmethodID methodId = + env->GetStaticMethodID(clazz, "setBlockingMethodCallsEnable", "(Z)Z"); + successful = env->CallStaticBooleanMethod(clazz, methodId, enable); + if (env->ExceptionCheck()) env->ExceptionClear(); + }); + return successful; +#elif TARGET_OS_IPHONE + return FIRInstanceIDSetBlockingMethodCallsEnable(enable); +#endif + return false; + } + + // Wait for the worker thread to start, returning true if the thread started, + // false otherwise. + static bool WaitForBlockedThread() { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + bool successful = false; + GetMockClass([&successful](JNIEnv* env, jclass clazz) { + jmethodID methodId = + env->GetStaticMethodID(clazz, "waitForBlockedThread", "()Z"); + successful = env->CallStaticBooleanMethod(clazz, methodId); + if (env->ExceptionCheck()) env->ExceptionClear(); + }); + return successful; +#elif TARGET_OS_IPHONE + return FIRInstanceIDWaitForBlockedThread(); +#endif + return false; + } + + // Validate the specified future handle is invalid. + template + static void ExpectInvalidFuture(const Future& future) { + EXPECT_THAT(future.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future.error_message(), IsNull()); + } + + App* app_ = nullptr; + firebase::testing::cppsdk::Reporter reporter_; + static const char* const kTokenEntity; + static const char* const kTokenScope; + static const char* const kTokenScopeAll; + static const int kMicrosecondsPerMillisecond; + // Default time to wait for future status changes. + static const int kFutureTimeoutMilliseconds; +}; + +const char* const InstanceIdTest::kTokenEntity = "an_entity"; +const char* const InstanceIdTest::kTokenScope = "a_scope"; +const char* const InstanceIdTest::kTokenScopeAll = "*"; +const int InstanceIdTest::kMicrosecondsPerMillisecond = 1000; +const int InstanceIdTest::kFutureTimeoutMilliseconds = 1000; + +// Validate creation of an InstanceId instance. +TEST_F(InstanceIdTest, TestCreate) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id, NotNull()); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); + delete instance_id; +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +// Test creation that fails. +TEST_F(InstanceIdTest, TestCreateWithError) { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("Failed to initialize"); +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeUnknown); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id, IsNull()); + EXPECT_THAT(init_result, Eq(kInitResultFailedMissingDependency)); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +// Ensure the retrieving the an InstanceId from the same app returns the same +// instance. +TEST_F(InstanceIdTest, TestCreateAndGet) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id1 = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id1, NotNull()); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); + auto* instance_id2 = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id2, Eq(instance_id1)); + delete instance_id1; +} + +// Validate InstanceId instance is destroyed when the corresponding app is +// destroyed. +// NOTE: It's not possible to execute this test on iOS as we can only create an +// instance ID object for the default app. +#if !TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestCreateAndDestroyApp) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + const char* kAppNames[] = {"named_app1", "named_app2"}; + auto* app = testing::CreateApp(testing::MockAppOptions(), kAppNames[0]); + auto* instance_id1 = InstanceId::GetInstanceId(app, &init_result); + EXPECT_THAT(instance_id1, NotNull()); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); + + // Temporarily disable LogAssert() which causes the application to assert. + void* log_callback_data; + LogCallback log_callback = LogGetCallback(&log_callback_data); + LogSetCallback( + [](LogLevel log_level, const char* log_message, void* callback_data) { + if (log_level == kLogLevelAssert) { + ASSERT_THAT( + log_message, + MatchesRegex( + "InstanceId object 0x[0-9A-Fa-f]+ should be " + "deleted before the App 0x[0-9A-Fa-f]+ it depends upon.")); + log_level = kLogLevelError; + } + reinterpret_cast(callback_data)(log_level, log_message, + nullptr); + }, + reinterpret_cast(log_callback)); + + delete app; // This should delete instance_id1's internal data, not + // instance_id1 itself. + EXPECT_THAT(instance_id1, NotNull()); + delete instance_id1; + + LogSetCallback(log_callback, log_callback_data); + + app = testing::CreateApp(testing::MockAppOptions(), kAppNames[1]); + // Validate the new app instance yields a new Instance ID object. + auto* instance_id2 = InstanceId::GetInstanceId(app, &init_result); + EXPECT_THAT(std::string(instance_id2->app().name()), + Eq(std::string(kAppNames[1]))); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); +} +#endif // !TARGET_OS_IPHONE + +TEST_F(InstanceIdTest, TestGetCreationTime) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); +#if !TARGET_OS_IPHONE + // At the moment creation_time() is not exposed on iOS. + AddExpectationAndroidIos("FirebaseInstanceId.getCreationTime", {}); +#endif // !TARGET_OS_IPHONE + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_THAT(instance_id->creation_time(), Eq(1512000287000)); +#else + EXPECT_THAT(instance_id->creation_time(), Eq(0)); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + delete instance_id; +} + +#if FIREBASE_PLATFORM_MOBILE +TEST_F(InstanceIdTest, TestGetId) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.getId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + const std::string expected_value("FakeId"); + CheckSuccessWithValue(instance_id->GetId(), expected_value); + CheckSuccessWithValue(instance_id->GetIdLastResult(), expected_value); + delete instance_id; +} +#endif // FIREBASE_PLATFORM_MOBILE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestGetIdTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.getId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->GetId(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +TEST_F(InstanceIdTest, TestDeleteId) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.deleteId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + CheckSuccess(instance_id->DeleteId()); + CheckSuccess(instance_id->DeleteIdLastResult()); + delete instance_id; +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteIdFailed) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + Error expected_error = kErrorUnknown; +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("Error while reading ID"); +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeNoAccess); + expected_error = kErrorNoAccess; +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + auto future = instance_id->DeleteId(); + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.error(), Eq(expected_error)); + EXPECT_THAT(future.error_message(), NotNull()); + delete instance_id; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteIdTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.deleteId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->DeleteId(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if FIREBASE_PLATFORM_MOBILE +TEST_F(InstanceIdTest, TestGetTokenEntityScope) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.getToken", + {kTokenEntity, kTokenScope}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + const std::string expected_value("FakeToken"); + CheckSuccessWithValue(instance_id->GetToken(kTokenEntity, kTokenScope), + expected_value); + CheckSuccessWithValue(instance_id->GetTokenLastResult(), expected_value); + delete instance_id; +} + +TEST_F(InstanceIdTest, TestGetToken) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.getToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + const std::string expected_value("FakeToken"); + CheckSuccessWithValue(instance_id->GetToken(), expected_value); + CheckSuccessWithValue(instance_id->GetTokenLastResult(), expected_value); + delete instance_id; +} + +// Sample code that creates an InstanceId for the default app and gets a token. +TEST_F(InstanceIdTest, TestGetTokenSample) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.getToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + // [START instance_id_get_token] + firebase::InitResult init_result; + auto* instance_id_object = firebase::instance_id::InstanceId::GetInstanceId( + firebase::App::GetInstance(), &init_result); + instance_id_object->GetToken().OnCompletion( + [](const firebase::Future& future) { + if (future.status() == kFutureStatusComplete && + future.error() == firebase::instance_id::kErrorNone) { + printf("Instance ID Token %s\n", future.result()->c_str()); + } + }); + // [END instance_id_get_token] + // WaitForFutureWithTimeout(instance_id_object->GetTokenLastResult()); + CheckSuccessWithValue(instance_id_object->GetTokenLastResult(), + std::string("FakeToken")); + delete instance_id_object; +} +#endif // FIREBASE_PLATFORM_MOBILE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestGetTokenFailed) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + Error expected_error = kErrorUnknown; +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("INSTANCE_ID_RESET"); + expected_error = kErrorIdInvalid; +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeAuthentication); + expected_error = kErrorAuthentication; +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + auto future = instance_id->GetToken(); + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.error(), Eq(expected_error)); + EXPECT_THAT(future.error_message(), NotNull()); + delete instance_id; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestGetTokenTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.getToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->GetToken(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +TEST_F(InstanceIdTest, TestDeleteTokenEntityScope) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.deleteToken", + {kTokenEntity, kTokenScope}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + CheckSuccess(instance_id->DeleteToken(kTokenEntity, kTokenScope)); + CheckSuccess(instance_id->DeleteTokenLastResult()); + delete instance_id; +} + +TEST_F(InstanceIdTest, TestDeleteToken) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.deleteToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + CheckSuccess(instance_id->DeleteToken()); + CheckSuccess(instance_id->DeleteTokenLastResult()); + delete instance_id; +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteTokenFailed) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("SERVICE_NOT_AVAILABLE"); +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeNoAccess); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + auto future = instance_id->DeleteToken(); + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.error(), Eq(kErrorNoAccess)); + EXPECT_THAT(future.error_message(), NotNull()); + delete instance_id; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteTokenTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.deleteToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->DeleteToken(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +} // namespace instance_id +} // namespace firebase diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java b/messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java new file mode 100644 index 0000000000..05e86e6466 --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java @@ -0,0 +1,82 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.content.Context; +import android.content.Intent; +import com.google.firebase.messaging.cpp.MessageWriter; +import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(GoogleRobolectricTestRunner.class) +public final class MessageForwardingServiceTest { + + @Mock private Context context; + @Mock private MessageWriter messageWriter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testHandleIntent() throws Exception { + Intent intent = new Intent(MessageForwardingService.ACTION_REMOTE_INTENT); + intent.putExtra("from", "from"); + intent.putExtra("google.message_id", "id"); + ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteMessage.class); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verify(messageWriter).writeMessage(any(), captor.capture(), eq(true), eq(null)); + RemoteMessage message = captor.getValue(); + assertThat(message.getMessageId()).isEqualTo("id"); + assertThat(message.getFrom()).isEqualTo("from"); + } + + @Test + public void testHandleIntent_noFrom() throws Exception { + Intent intent = new Intent(MessageForwardingService.ACTION_REMOTE_INTENT); + intent.putExtra("from", "from"); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verifyZeroInteractions(messageWriter); + } + + @Test + public void testHandleIntent_noId() throws Exception { + Intent intent = new Intent(MessageForwardingService.ACTION_REMOTE_INTENT); + intent.putExtra("google.message_id", "id"); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verifyZeroInteractions(messageWriter); + } + + @Test + public void testHandleIntent_wrongAction() throws Exception { + Intent intent = new Intent("wrong_action"); + intent.putExtra("from", "from"); + intent.putExtra("google.message_id", "id"); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verifyZeroInteractions(messageWriter); + } +} diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java b/messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java new file mode 100644 index 0000000000..64ce0ad5c2 --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java @@ -0,0 +1,31 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import android.os.Bundle; + +/** */ +public class RemoteMessageUtil { + + private RemoteMessageUtil() {} // Utility class. + + public static RemoteMessage remoteMessage(Bundle bundle) { + return new RemoteMessage(bundle); + } + + public static SendException sendException(String reason) { + return new SendException(reason); + } +} diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java new file mode 100644 index 0000000000..ff7db3b4ed --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java @@ -0,0 +1,86 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging.cpp; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.verify; + +import android.net.Uri; +import android.os.Bundle; +import com.google.firebase.messaging.RemoteMessage; +import com.google.firebase.messaging.RemoteMessageUtil; +import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(GoogleRobolectricTestRunner.class) +public final class ListenerServiceTest { + + @Mock private MessageWriter messageWriter; + + private ListenerService listenerService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + listenerService = new ListenerService(messageWriter); + } + + @Test + public void testOnDeletedMessages() throws Exception { + listenerService.onDeletedMessages(); + verify(messageWriter) + .writeMessageEventToInternalStorage( + eq(listenerService), + (String) isNull(), + eq(ListenerService.MESSAGE_TYPE_DELETED), + (String) isNull()); + } + + @Test + public void testOnMessageReceived() { + RemoteMessage message = RemoteMessageUtil.remoteMessage(new Bundle()); + listenerService.onMessageReceived(message); + verify(messageWriter).writeMessage(any(), eq(message), eq(false), (Uri) isNull()); + } + + @Test + public void testOnMessageSent() { + listenerService.onMessageSent("message_id"); + verify(messageWriter) + .writeMessageEventToInternalStorage( + eq(listenerService), + eq("message_id"), + eq(ListenerService.MESSAGE_TYPE_SEND_EVENT), + (String) isNull()); + } + + @Test + public void testOnSendError() { + listenerService.onSendError( + "message_id", RemoteMessageUtil.sendException("service_not_available")); + verify(messageWriter) + .writeMessageEventToInternalStorage( + eq(listenerService), + eq("message_id"), + eq(ListenerService.MESSAGE_TYPE_SEND_ERROR), + eq("com.google.firebase.messaging.SendException: service_not_available")); + } +} diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java new file mode 100644 index 0000000000..7621a0b0be --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java @@ -0,0 +1,91 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging.cpp; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Bundle; +import com.google.common.io.ByteStreams; +import com.google.firebase.messaging.RemoteMessage; +import com.google.firebase.messaging.RemoteMessageUtil; +import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(GoogleRobolectricTestRunner.class) +public final class MessageWriterTest { + + private static final Path STORAGE_FILE_PATH = Paths.get("/tmp/" + MessageWriter.STORAGE_FILE); + + @Mock private Context context; + private MessageWriter messageWriter; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + messageWriter = new MessageWriter(); + Files.deleteIfExists(STORAGE_FILE_PATH); + when(context.openFileOutput(eq(MessageWriter.LOCK_FILE), anyInt())) + .thenReturn(new FileOutputStream("/tmp/" + MessageWriter.LOCK_FILE)); + when(context.openFileOutput(eq(MessageWriter.STORAGE_FILE), anyInt())) + .thenReturn(new FileOutputStream(STORAGE_FILE_PATH.toFile(), true)); + } + + @Test + public void testMessageWriter() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString("from", "my_from"); + bundle.putString("google.message_id", "my_message_id"); + bundle.putString("some_data", "my_data"); + bundle.putString("collapse_key", "a_key"); + bundle.putString("google.priority", "high"); + bundle.putString("google.original_priority", "normal"); + bundle.putLong("google.sent_time", 1234); + bundle.putInt("google.ttl", 8765); + RemoteMessage message = RemoteMessageUtil.remoteMessage(bundle); + messageWriter.writeMessage(context, message, false, null); + ByteBuffer byteBuffer = ByteBuffer.wrap(readStorageFile()).order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.getInt(); // Discard size. + SerializedEvent event = SerializedEvent.getRootAsSerializedEvent(byteBuffer); + SerializedMessage result = (SerializedMessage) event.event(new SerializedMessage()); + assertThat(result.from()).isEqualTo("my_from"); + assertThat(result.messageId()).isEqualTo("my_message_id"); + assertThat(result.collapseKey()).isEqualTo("a_key"); + assertThat(result.priority()).isEqualTo("high"); + assertThat(result.originalPriority()).isEqualTo("normal"); + assertThat(result.sentTime()).isEqualTo(1234); + assertThat(result.timeToLive()).isEqualTo(8765); + assertThat(result.data(0).key()).isEqualTo("some_data"); + assertThat(result.data(0).value()).isEqualTo("my_data"); + } + + private byte[] readStorageFile() throws Exception { + return ByteStreams.toByteArray(new FileInputStream(STORAGE_FILE_PATH.toFile())); + } +} diff --git a/messaging/src/ios/fake/FIRMessaging.h b/messaging/src/ios/fake/FIRMessaging.h new file mode 100644 index 0000000000..802baa97d6 --- /dev/null +++ b/messaging/src/ios/fake/FIRMessaging.h @@ -0,0 +1,507 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import + +/** + * @related FIRMessaging + * + * The completion handler invoked when the registration token returns. + * If the call fails we return the appropriate `error code`, described by + * `FIRMessagingError`. + * + * @param FCMToken The valid registration token returned by FCM. + * @param error The error describing why a token request failed. The error code + * will match a value from the FIRMessagingError enumeration. + */ +typedef void(^FIRMessagingFCMTokenFetchCompletion)(NSString * _Nullable FCMToken, + NSError * _Nullable error) + NS_SWIFT_NAME(MessagingFCMTokenFetchCompletion); + + +/** + * @related FIRMessaging + * + * The completion handler invoked when the registration token deletion request is + * completed. If the call fails we return the appropriate `error code`, described + * by `FIRMessagingError`. + * + * @param error The error describing why a token deletion failed. The error code + * will match a value from the FIRMessagingError enumeration. + */ +typedef void(^FIRMessagingDeleteFCMTokenCompletion)(NSError * _Nullable error) + NS_SWIFT_NAME(MessagingDeleteFCMTokenCompletion); + +/** + * Callback to invoke once the HTTP call to FIRMessaging backend for updating + * subscription finishes. + * + * @param error The error which occurred while updating the subscription topic + * on the FIRMessaging server. This will be nil in case the operation + * was successful, or if the operation was cancelled. + */ +typedef void (^FIRMessagingTopicOperationCompletion)(NSError *_Nullable error); + +/** + * The completion handler invoked once the data connection with FIRMessaging is + * established. The data connection is used to send a continous stream of + * data and all the FIRMessaging data notifications arrive through this connection. + * Once the connection is established we invoke the callback with `nil` error. + * Correspondingly if we get an error while trying to establish a connection + * we invoke the handler with an appropriate error object and do an + * exponential backoff to try and connect again unless successful. + * + * @param error The error object if any describing why the data connection + * to FIRMessaging failed. + */ +typedef void(^FIRMessagingConnectCompletion)(NSError * __nullable error) + NS_SWIFT_NAME(MessagingConnectCompletion) + __deprecated_msg("Please listen for the FIRMessagingConnectionStateChangedNotification " + "NSNotification instead."); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + * Notification sent when the upstream message has been delivered + * successfully to the server. The notification object will be the messageID + * of the successfully delivered message. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendSuccessNotification + NS_SWIFT_NAME(MessagingSendSuccess); + +/** + * Notification sent when the upstream message was failed to be sent to the + * server. The notification object will be the messageID of the failed + * message. The userInfo dictionary will contain the relevant error + * information for the failure. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendErrorNotification + NS_SWIFT_NAME(MessagingSendError); + +/** + * Notification sent when the Firebase messaging server deletes pending + * messages due to exceeded storage limits. This may occur, for example, when + * the device cannot be reached for an extended period of time. + * + * It is recommended to retrieve any missing messages directly from the + * server. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingMessagesDeletedNotification + NS_SWIFT_NAME(MessagingMessagesDeleted); + +/** + * Notification sent when Firebase Messaging establishes or disconnects from + * an FCM socket connection. You can query the connection state in this + * notification by checking the `isDirectChannelEstablished` property of FIRMessaging. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingConnectionStateChangedNotification + NS_SWIFT_NAME(MessagingConnectionStateChanged); + +/** + * Notification sent when the FCM registration token has been refreshed. You can also + * receive the FCM token via the FIRMessagingDelegate method + * `-messaging:didReceiveRegistrationToken:` + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull + FIRMessagingRegistrationTokenRefreshedNotification + NS_SWIFT_NAME(MessagingRegistrationTokenRefreshed); +#else +/** + * Notification sent when the upstream message has been delivered + * successfully to the server. The notification object will be the messageID + * of the successfully delivered message. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendSuccessNotification + NS_SWIFT_NAME(MessagingSendSuccessNotification); + +/** + * Notification sent when the upstream message was failed to be sent to the + * server. The notification object will be the messageID of the failed + * message. The userInfo dictionary will contain the relevant error + * information for the failure. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendErrorNotification + NS_SWIFT_NAME(MessagingSendErrorNotification); + +/** + * Notification sent when the Firebase messaging server deletes pending + * messages due to exceeded storage limits. This may occur, for example, when + * the device cannot be reached for an extended period of time. + * + * It is recommended to retrieve any missing messages directly from the + * server. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingMessagesDeletedNotification + NS_SWIFT_NAME(MessagingMessagesDeletedNotification); + +/** + * Notification sent when Firebase Messaging establishes or disconnects from + * an FCM socket connection. You can query the connection state in this + * notification by checking the `isDirectChannelEstablished` property of FIRMessaging. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingConnectionStateChangedNotification + NS_SWIFT_NAME(MessagingConnectionStateChangedNotification); + +/** + * Notification sent when the FCM registration token has been refreshed. You can also + * receive the FCM token via the FIRMessagingDelegate method + * `-messaging:didReceiveRegistrationToken:` + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingRegistrationTokenRefreshedNotification + NS_SWIFT_NAME(MessagingRegistrationTokenRefreshedNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** + * @enum FIRMessagingError + */ +typedef NS_ENUM(NSUInteger, FIRMessagingError) { + /// Unknown error. + FIRMessagingErrorUnknown = 0, + + /// FIRMessaging couldn't validate request from this client. + FIRMessagingErrorAuthentication = 1, + + /// InstanceID service cannot be accessed. + FIRMessagingErrorNoAccess = 2, + + /// Request to InstanceID backend timed out. + FIRMessagingErrorTimeout = 3, + + /// No network available to reach the servers. + FIRMessagingErrorNetwork = 4, + + /// Another similar operation in progress, bailing this one. + FIRMessagingErrorOperationInProgress = 5, + + /// Some parameters of the request were invalid. + FIRMessagingErrorInvalidRequest = 7, +} NS_SWIFT_NAME(MessagingError); + +/// Status for the downstream message received by the app. +typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) { + /// Unknown status. + FIRMessagingMessageStatusUnknown, + /// New downstream message received by the app. + FIRMessagingMessageStatusNew, +} NS_SWIFT_NAME(MessagingMessageStatus); + +/** + * The APNS token type for the app. If the token type is set to `UNKNOWN` + * Firebase Messaging will implicitly try to figure out what the actual token type + * is from the provisioning profile. + * Unless you really need to specify the type, you should use the `APNSToken` + * property instead. + */ +typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) { + /// Unknown token type. + FIRMessagingAPNSTokenTypeUnknown, + /// Sandbox token type. + FIRMessagingAPNSTokenTypeSandbox, + /// Production token type. + FIRMessagingAPNSTokenTypeProd, +} NS_SWIFT_NAME(MessagingAPNSTokenType); + +/// Information about a downstream message received by the app. +NS_SWIFT_NAME(MessagingMessageInfo) +@interface FIRMessagingMessageInfo : NSObject + +/// The status of the downstream message +@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status; + +@end + +/** + * A remote data message received by the app via FCM (not just the APNs interface). + * + * This is only for devices running iOS 10 or above. To support devices running iOS 9 or below, use + * the local and remote notifications handlers defined in UIApplicationDelegate protocol. + */ +NS_SWIFT_NAME(MessagingRemoteMessage) +@interface FIRMessagingRemoteMessage : NSObject + +/// The downstream message received by the application. +@property(nonatomic, readonly, strong, nonnull) NSDictionary *appData; +@end + +@class FIRMessaging; +/** + * A protocol to handle events from FCM for devices running iOS 10 or above. + * + * To support devices running iOS 9 or below, use the local and remote notifications handlers + * defined in UIApplicationDelegate protocol. + */ +NS_SWIFT_NAME(MessagingDelegate) +@protocol FIRMessagingDelegate + +/// This method will be called whenever FCM receives a new, default FCM token for your +/// Firebase project's Sender ID. +/// You can send this token to your application server to send notifications to this device. +- (void)messaging:(nonnull FIRMessaging *)messaging + didReceiveRegistrationToken:(nonnull NSString *)fcmToken + NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:)); + +@optional +/// This method is called on iOS 10 devices to handle data messages received via FCM through its +/// direct channel (not via APNS). For iOS 9 and below, the FCM data message is delivered via the +/// UIApplicationDelegate's -application:didReceiveRemoteNotification: method. +- (void)messaging:(nonnull FIRMessaging *)messaging + didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage + NS_SWIFT_NAME(messaging(_:didReceive:)) + __IOS_AVAILABLE(10.0); + +/// The callback to handle data message received via FCM for devices running iOS 10 or above. +- (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage + NS_SWIFT_NAME(application(received:)) + __deprecated_msg("Use FIRMessagingDelegate’s -messaging:didReceiveMessage:"); + +@end + +/** + * Firebase Messaging lets you reliably deliver messages at no cost. + * + * To send or receive messages, the app must get a + * registration token from FIRInstanceID. This token authorizes an + * app server to send messages to an app instance. + * + * In order to receive FIRMessaging messages, declare `application:didReceiveRemoteNotification:`. + */ +NS_SWIFT_NAME(Messaging) +@interface FIRMessaging : NSObject + +/** + * Delegate to handle FCM token refreshes, and remote data messages received via FCM for devices + * running iOS 10 or above. + */ +@property(nonatomic, weak, nullable) id delegate; + + +/** + * Delegate to handle remote data messages received via FCM for devices running iOS 10 or above. + */ +@property(nonatomic, weak, nullable) id remoteMessageDelegate + __deprecated_msg("Use 'delegate' property"); + +/** + * When set to YES, Firebase Messaging will automatically establish a socket-based, direct channel + * to the FCM server. You only need to enable this if you are sending upstream messages or + * receiving non-APNS, data-only messages in foregrounded apps. + * Default is NO. + */ +@property(nonatomic) BOOL shouldEstablishDirectChannel; + +/** + * Returns YES if the direct channel to the FCM server is active, NO otherwise. + */ +@property(nonatomic, readonly) BOOL isDirectChannelEstablished; + +/** + * FIRMessaging + * + * @return An instance of FIRMessaging. + */ ++ (nonnull instancetype)messaging NS_SWIFT_NAME(messaging()); + +/** + * Unavailable. Use +messaging instead. + */ +- (nonnull instancetype)init __attribute__((unavailable("Use +messaging instead."))); + +#pragma mark - APNS + +/** + * This property is used to set the APNS Token received by the application delegate. + * + * FIRMessaging uses method swizzling to ensure the APNS token is set automatically. + * However, if you have disabled swizzling by setting `FirebaseAppDelegateProxyEnabled` + * to `NO` in your app's Info.plist, you should manually set the APNS token in your + * application delegate's -application:didRegisterForRemoteNotificationsWithDeviceToken: + * method. + */ +@property(nonatomic, copy, nullable) NSData *APNSToken NS_SWIFT_NAME(apnsToken); + +#pragma mark - FCM Tokens + +/** + * The FCM token is used to identify this device so that FCM can send notifications to it. + * It is associated with your APNS token when the APNS token is supplied, so that sending + * messages to the FCM token will be delivered over APNS. + * + * The FCM token is sometimes refreshed automatically. You can be notified of these changes + * via the FIRMessagingDelegate method `-message:didReceiveRegistrationToken:`, or by + * listening for the `FIRMessagingRegistrationTokenRefreshedNotification` notification. + * + * Once you have an FCM token, you should send it to your application server, so it can use + * the FCM token to send notifications to your device. + */ +@property(nonatomic, readwrite, nullable) NSString *FCMToken NS_SWIFT_NAME(fcmToken); + +/** + * Is Firebase Messaging token auto generation enabled? If this flag is disabled, + * Firebase Messaging will not generate token automatically for message delivery. + * + * If this flag is disabled, Firebase Messaging does not generate new tokens automatically for + * message delivery. If this flag is enabled, FCM generates a registration token on application + * start when there is no existing valid token. FCM also generates a new token when an existing + * token is deleted. + * + * This setting is persisted, and is applied on future + * invocations of your application. Once explicitly set, it overrides any + * settings in your Info.plist. + * + * By default, FCM automatic initialization is enabled. If you need to change the + * default (for example, because you want to prompt the user before getting token) + * set FirebaseMessagingAutoInitEnabled to false in your application's Info.plist. + */ +@property(nonatomic, assign, getter=isAutoInitEnabled) BOOL autoInitEnabled; + +/** + * Retrieves an FCM registration token for a particular Sender ID. This registration token is + * not cached by FIRMessaging. FIRMessaging should have an APNS token set before calling this + * to ensure that notifications can be delivered via APNS using this FCM token. You may + * re-retrieve the FCM token once you have the APNS token set, to associate it with the FCM + * token. The default FCM token is automatically associated with the APNS token, if the APNS + * token data is available. + * + * @param senderID The Sender ID for a particular Firebase project. + * @param completion The completion handler to handle the token request. + */ +- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID + completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion + NS_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:)); + + +/** + * Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send + * notifications to that FCM token. + * + * @param senderID The senderID for a particular Firebase project. + * @param completion The completion handler to handle the token deletion. + */ +- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID + completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion + NS_SWIFT_NAME(deleteFCMToken(forSenderID:completion:)); + + +#pragma mark - Connect + +/** + * Create a FIRMessaging data connection which will be used to send the data notifications + * sent by your server. It will also be used to send ACKS and other messages based + * on the FIRMessaging ACKS and other messages based on the FIRMessaging protocol. + * + * + * @param handler The handler to be invoked once the connection is established. + * If the connection fails we invoke the handler with an + * appropriate error code letting you know why it failed. At + * the same time, FIRMessaging performs exponential backoff to retry + * establishing a connection and invoke the handler when successful. + */ +- (void)connectWithCompletion:(nonnull FIRMessagingConnectCompletion)handler + NS_SWIFT_NAME(connect(handler:)) + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead."); + +/** + * Disconnect the current FIRMessaging data connection. This stops any attempts to + * connect to FIRMessaging. Calling this on an already disconnected client is a no-op. + * + * Call this before `teardown` when your app is going to the background. + * Since the FIRMessaging connection won't be allowed to live when in background it is + * prudent to close the connection. + */ +- (void)disconnect + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead."); + +#pragma mark - Topics + +/** + * Asynchronously subscribes to a topic. + * + * @param topic The name of the topic, for example, @"sports". + */ +- (void)subscribeToTopic:(nonnull NSString *)topic NS_SWIFT_NAME(subscribe(toTopic:)); + +/** + * Asynchronously subscribes to a topic. + * + * @param topic The name of the topic, for example, @"sports". + * @param completion The completion that is invoked once the subscribe call ends. In case of + * success, nil error is returned. Otherwise, an appropriate error object is + * returned. + */ +- (void)subscribeToTopic:(nonnull NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion; + +/** + * Asynchronously unsubscribe from a topic. + * + * @param topic The name of the topic, for example @"sports". + */ +- (void)unsubscribeFromTopic:(nonnull NSString *)topic NS_SWIFT_NAME(unsubscribe(fromTopic:)); + +/** + * Asynchronously unsubscribe from a topic. + * + * @param topic The name of the topic, for example @"sports". + * @param completion The completion that is invoked once the subscribe call ends. In case of + * success, nil error is returned. Otherwise, an appropriate error object is + * returned. + */ +- (void)unsubscribeFromTopic:(nonnull NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion; + +#pragma mark - Upstream + +/** + * Sends an upstream ("device to cloud") message. + * + * The message is queued if we don't have an active connection. + * You can only use the upstream feature if your FCM implementation + * uses the XMPP server protocol. + * + * @param message Key/Value pairs to be sent. Values must be String, any + * other type will be ignored. + * @param to A string identifying the receiver of the message. For FCM + * project IDs the value is `SENDER_ID@gcm.googleapis.com`. + * @param messageID The ID of the message. This is generated by the application. It + * must be unique for each message generated by this application. + * It allows error callbacks and debugging, to uniquely identify + * each message. + * @param ttl The time to live for the message. In case we aren't able to + * send the message before the TTL expires we will send you a + * callback. If 0, we'll attempt to send immediately and return + * an error if we're not connected. Otherwise, the message will + * be queued. As for server-side messages, we don't return an error + * if the message has been dropped because of TTL; this can happen + * on the server side, and it would require extra communication. + */ +- (void)sendMessage:(nonnull NSDictionary *)message + to:(nonnull NSString *)receiver + withMessageID:(nonnull NSString *)messageID + timeToLive:(int64_t)ttl; + +#pragma mark - Analytics + +/** + * Use this to track message delivery and analytics for messages, typically + * when you receive a notification in `application:didReceiveRemoteNotification:`. + * However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled` + * flag to NO in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing + * or set to YES in your Info.plist, the library will call this automatically. + * + * @param message The downstream message received by the application. + * + * @return Information about the downstream message. + */ +- (nonnull FIRMessagingMessageInfo *)appDidReceiveMessage:(nonnull NSDictionary *)message; + +@end diff --git a/messaging/src/ios/fake/FIRMessaging.mm b/messaging/src/ios/fake/FIRMessaging.mm new file mode 100644 index 0000000000..9ef71450fa --- /dev/null +++ b/messaging/src/ios/fake/FIRMessaging.mm @@ -0,0 +1,125 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "messaging/src/ios/fake/FIRMessaging.h" + +#include "testing/reporter_impl.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRMessagingMessageInfo + +- (instancetype)initWithStatus:(FIRMessagingMessageStatus)status { + self = [super init]; + if (self) { + _status = status; + } + return self; +} + +@end + +@implementation FIRMessaging + +- (instancetype)initInternal { + self = [super init]; + return self; +} + ++ (instancetype)messaging { + static FIRMessaging *messaging; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Start Messaging (Fully initialize in one place). + messaging = [[FIRMessaging alloc] initInternal]; + }); + return messaging; +} + +BOOL is_auto_init_enabled = true; + +- (BOOL)isAutoInitEnabled { + return is_auto_init_enabled; +} + +- (void)setAutoInitEnabled:(BOOL)autoInitEnabled { + is_auto_init_enabled = autoInitEnabled; +} + +- (void)retrieveFCMTokenForSenderID:(NSString *)senderID + completion:(FIRMessagingFCMTokenFetchCompletion)completion + NS_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:)) {} + +- (void)deleteFCMTokenForSenderID:(NSString *)senderID + completion:(FIRMessagingDeleteFCMTokenCompletion)completion + NS_SWIFT_NAME(deleteFCMToken(forSenderID:completion:)) {} + +- (void)connectWithCompletion:(FIRMessagingConnectCompletion)handler + NS_SWIFT_NAME(connect(handler:)) + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.") {} + +- (void)disconnect + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.") {} + ++ (NSString *)normalizeTopic:(NSString *)topic { + return topic; +} + +- (void)subscribeToTopic:(NSString *)topic NS_SWIFT_NAME(subscribe(toTopic:)) { + static const char fake[] = "-[FIRMessaging subscribeToTopic:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} + +- (void)subscribeToTopic:(NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion { + static const char fake[] = "-[FIRMessaging subscribeToTopic:completion:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} + +- (void)unsubscribeFromTopic:(NSString *)topic NS_SWIFT_NAME(unsubscribe(fromTopic:)) { + static const char fake[] = "-[FIRMessaging unsubscribeFromTopic:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} +- (void)unsubscribeFromTopic:(NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion { + static const char fake[] = "-[FIRMessaging unsubscribeFromTopic:completion:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} + +- (void)sendMessage:(NSDictionary *)message + to:(NSString *)receiver + withMessageID:(NSString *)messageID + timeToLive:(int64_t)ttl { + FakeReporter->AddReport("-[FIRMessaging sendMessage:to:withMessageID:timeToLive:]", + { receiver.UTF8String, messageID.UTF8String, + [NSString stringWithFormat:@"%lld", ttl].UTF8String }); +} + +- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message { + FIRMessagingMessageInfo *info = + [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusUnknown]; + return info; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java b/messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java new file mode 100644 index 0000000000..f5f96392b8 --- /dev/null +++ b/messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java @@ -0,0 +1,81 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import android.text.TextUtils; +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.FakeReporter; +import java.util.ArrayList; + +/** + * Fake FirebaseMessaging class. + */ +public class FirebaseMessaging { + + public static synchronized FirebaseMessaging getInstance() { + return new FirebaseMessaging(); + } + + public boolean isAutoInitEnabled() { + return autoInitEnabled_; + } + + public void setAutoInitEnabled(boolean enable) { + autoInitEnabled_ = enable; + } + + public void send(RemoteMessage message) { + FakeReporter.addReport("FirebaseMessaging.send", String.valueOf(message.to), + String.valueOf(message.data), String.valueOf(message.messageId), + String.valueOf(message.from), String.valueOf(message.ttl)); + if (TextUtils.isEmpty(message.to)) { + throw new IllegalArgumentException("Missing 'to'"); + } + } + + public Task subscribeToTopic(String topic) { + String fake = "FirebaseMessaging.subscribeToTopic"; + ArrayList args = new ArrayList<>(FakeReporter.getFakeArgs(fake)); + args.add(String.valueOf(topic)); + FakeReporter.addReport(fake, args.toArray(new String[0])); + if (TextUtils.isEmpty(topic) || "$invalid".equals(topic)) { + throw new IllegalArgumentException("Invalid topic: " + topic); + } + return Task.forResult(fake, null); + } + + public Task unsubscribeFromTopic(String topic) { + String fake = "FirebaseMessaging.unsubscribeFromTopic"; + ArrayList args = new ArrayList<>(FakeReporter.getFakeArgs(fake)); + args.add(String.valueOf(topic)); + FakeReporter.addReport(fake, args.toArray(new String[0])); + if (TextUtils.isEmpty(topic) || "$invalid".equals(topic)) { + throw new IllegalArgumentException("Invalid topic: " + topic); + } + return Task.forResult(fake, null); + } + + public void setDeliveryMetricsExportToBigQuery(boolean enable) { + deliveryMetricsExportToBigQueryEnabled = enable; + } + + public boolean deliveryMetricsExportToBigQueryEnabled() { + return deliveryMetricsExportToBigQueryEnabled; + } + + private boolean deliveryMetricsExportToBigQueryEnabled = false; + + private boolean autoInitEnabled_ = true; +} diff --git a/messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java b/messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java new file mode 100644 index 0000000000..b65cd5e9b5 --- /dev/null +++ b/messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java @@ -0,0 +1,73 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import java.util.Map; + +/** + * Fake RemoteMessage class. + */ +public class RemoteMessage { + + public final String from; + public final String to; + public final Map data; + public final Integer ttl; + public final String messageId; + + private RemoteMessage( + String from, String to, Map data, Integer ttl, String messageId) { + this.from = from; + this.to = to; + this.data = data; + this.ttl = ttl; + this.messageId = messageId; + } + + /** + * Fake Builder class. + */ + public static class Builder { + + private final String to; + private Map data; + private Integer ttl; + private String messageId; + private String from; + + public Builder(String to) { + this.to = to; + } + + public Builder setData(Map data) { + this.data = data; + return this; + } + + public Builder setTtl(int ttl) { + this.ttl = ttl; + return this; + } + + public Builder setMessageId(String messageId) { + this.messageId = messageId; + return this; + } + + public RemoteMessage build() { + return new RemoteMessage("my_from", to, data, ttl, messageId); + } + } +} diff --git a/messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java b/messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java new file mode 100644 index 0000000000..d7a7efc6ae --- /dev/null +++ b/messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java @@ -0,0 +1,21 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging.cpp; + +/** + * Fake RegistrationIntentService class. + */ +public class RegistrationIntentService { +} diff --git a/messaging/tests/CMakeLists.txt b/messaging/tests/CMakeLists.txt new file mode 100644 index 0000000000..afad30765d --- /dev/null +++ b/messaging/tests/CMakeLists.txt @@ -0,0 +1,78 @@ +# Copyright 2019 Google LLC +# +# 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. + +if(ANDROID OR IOS) + set(messaging_test_util_common_SRCS + messaging_test_util.h) + set(messaging_test_util_android_SRCS + android/messaging_test_util.cc) + set(messaging_test_util_ios_SRCS + ios/messaging_test_util.mm) + + if(ANDROID) + set(messaging_test_util_SRCS + "${messaging_test_util_common_SRCS}" + "${messaging_test_util_android_SRCS}") + elseif(IOS) + set(messaging_test_util_SRCS + "${messaging_test_util_common_SRCS}" + "${messaging_test_util_ios_SRCS}") + else() + set(messaging_test_util_SRCS + "") + endif() + + add_library(firebase_messaging_test_util STATIC + ${messaging_test_util_SRCS}) + + target_include_directories(firebase_messaging_test_util + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/include + ${FIREBASE_GEN_FILE_DIR} + ) + + target_link_libraries(firebase_messaging_test_util + PRIVATE + gtest + gmock + ) + + firebase_cpp_cc_test( + firebase_messaging_test + SOURCES + messaging_test.cc + DEPENDS + firebase_app_for_testing + firebase_messaging + firebase_messaging_test_util + firebase_testing + ) + + firebase_cpp_cc_test_on_ios( + firebase_messaging_test + HOST + firebase_app_for_testing_ios + SOURCES + messaging_test.cc + DEPENDS + firebase_messaging + firebase_messaging_test_util + firebase_testing + CUSTOM_FRAMEWORKS + FirebaseMessaging + Protobuf + ) + +endif() diff --git a/messaging/tests/android/cpp/message_reader_test.cc b/messaging/tests/android/cpp/message_reader_test.cc new file mode 100644 index 0000000000..011f33a9e2 --- /dev/null +++ b/messaging/tests/android/cpp/message_reader_test.cc @@ -0,0 +1,289 @@ +// Copyright 2019 Google LLC +// +// 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 +#include +#include + +#include "app/src/util.h" +#include "messaging/messaging_generated.h" +#include "messaging/src/android/cpp/message_reader.h" +#include "messaging/src/include/firebase/messaging.h" +#include "gtest/gtest.h" + +// Since we're compiling a subset of the Android library on all platforms, +// we need to register a stub module initializer referenced by messaging.h +// to satisfy the linker. +FIREBASE_APP_REGISTER_CALLBACKS(messaging, { return kInitResultSuccess; }, {}); + +namespace firebase { +namespace messaging { +namespace internal { + +using com::google::firebase::messaging::cpp::CreateDataPairDirect; +using com::google::firebase::messaging::cpp::CreateSerializedEvent; +using com::google::firebase::messaging::cpp::CreateSerializedMessageDirect; +using com::google::firebase::messaging::cpp::CreateSerializedNotificationDirect; +using com::google::firebase::messaging::cpp::CreateSerializedTokenReceived; +using com::google::firebase::messaging::cpp::DataPair; +using com::google::firebase::messaging::cpp::FinishSerializedEventBuffer; +using com::google::firebase::messaging::cpp::SerializedEventUnion; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedMessage; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedTokenReceived; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_MAX; +using flatbuffers::FlatBufferBuilder; + +class MessageReaderTest : public ::testing::Test { + protected: + void SetUp() override {} + + void TearDown() override { + messages_received_.clear(); + tokens_received_.clear(); + } + + // Stores the message in this class. + static void MessageReceived(const Message& message, void* callback_data) { + MessageReaderTest* test = + reinterpret_cast(callback_data); + test->messages_received_.push_back(message); + } + + // Stores the token in this class. + static void TokenReceived(const char* token, void* callback_data) { + MessageReaderTest* test = + reinterpret_cast(callback_data); + test->tokens_received_.push_back(std::string(token)); + } + + protected: + // Messages received by MessageReceived(). + std::vector messages_received_; + // Tokens received by TokenReceived(). + std::vector tokens_received_; +}; + +TEST_F(MessageReaderTest, Construct) { + MessageReader reader( + MessageReaderTest::MessageReceived, reinterpret_cast(1), + MessageReaderTest::TokenReceived, reinterpret_cast(2)); + EXPECT_EQ(reinterpret_cast(MessageReaderTest::MessageReceived), + reinterpret_cast(reader.message_callback())); + EXPECT_EQ(reinterpret_cast(1), reader.message_callback_data()); + EXPECT_EQ(reinterpret_cast(MessageReaderTest::TokenReceived), + reinterpret_cast(reader.token_callback())); + EXPECT_EQ(reinterpret_cast(2), reader.token_callback_data()); +} + +// Read an empty buffer and ensure no data is parsed. +TEST_F(MessageReaderTest, ReadFromBufferEmpty) { + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(std::string()); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Read from a buffer that is too small and ensure no data is parsed. +TEST_F(MessageReaderTest, ReadFromBufferTooSmall) { + std::string buffer; + buffer.push_back('b'); + buffer.push_back('d'); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Read from a buffer with a header length that overflows the buffer size. +TEST_F(MessageReaderTest, ReadFromBufferHeaderOverflow) { + int32_t header = 9; + std::string buffer; + buffer.resize(sizeof(header)); + memcpy(&buffer[0], reinterpret_cast(&header), sizeof(header)); + buffer.push_back('5'); + buffer.push_back('6'); + buffer.push_back('7'); + buffer.push_back('8'); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Append a FlatBuffer to a string with the size of the FlatBuffer stored +// in a 32-bit integer header. +void AppendFlatBufferToString(std::string* output, + const FlatBufferBuilder& fbb) { + int32_t flatbuffer_size = static_cast(fbb.GetSize()); + size_t buffer_offset = output->size(); + output->resize(buffer_offset + sizeof(flatbuffer_size) + flatbuffer_size); + *(reinterpret_cast(&((*output)[buffer_offset]))) = flatbuffer_size; + memcpy(&((*output)[buffer_offset + sizeof(flatbuffer_size)]), + fbb.GetBufferPointer(), flatbuffer_size); +} + +// Read tokens from a buffer. +TEST_F(MessageReaderTest, ReadFromBufferTokenReceived) { + std::string buffer; + std::string tokens[3]; + tokens[0] = "token1"; + tokens[1] = "token2"; + tokens[2] = "token3"; + for (size_t i = 0; i < sizeof(tokens) / sizeof(tokens[0]); ++i) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedTokenReceived, + CreateSerializedTokenReceived(fbb, fbb.CreateString(tokens[i])) + .Union())); + AppendFlatBufferToString(&buffer, fbb); + } + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(3, tokens_received_.size()); + EXPECT_EQ(tokens[0], tokens_received_[0]); + EXPECT_EQ(tokens[1], tokens_received_[1]); + EXPECT_EQ(tokens[2], tokens_received_[2]); +} + +// Read a message from a buffer. +TEST_F(MessageReaderTest, ReadFromBufferMessageReceived) { + FlatBufferBuilder fbb; + std::vector> data; + data.push_back(CreateDataPairDirect(fbb, "foo", "bar")); + data.push_back(CreateDataPairDirect(fbb, "bosh", "bash")); + std::vector> body_loc_args; + body_loc_args.push_back(fbb.CreateString("1")); + body_loc_args.push_back(fbb.CreateString("2")); + std::vector> title_loc_args; + title_loc_args.push_back(fbb.CreateString("3")); + title_loc_args.push_back(fbb.CreateString("4")); + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedMessage, + CreateSerializedMessageDirect( + fbb, "from:bob", "to:jane", "collapsekey", &data, "rawdata", + "message_id", "message_type", + "high", // priority + 10, // TTL + "error0", "an error description", + CreateSerializedNotificationDirect( + fbb, "title", "body", "icon", "sound", "badge", "tag", + "color", "click_action", "body_loc_key", &body_loc_args, + "title_loc_key", &title_loc_args, "android_channel_id"), + true, // opened + "http://alink.com", + 1234, // sent time + "normal" /* original_priority */) + .Union())); + std::string buffer; + AppendFlatBufferToString(&buffer, fbb); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(1, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); + Message& message = messages_received_[0]; + EXPECT_EQ("from:bob", message.from); + EXPECT_EQ("to:jane", message.to); + EXPECT_EQ("collapsekey", message.collapse_key); + EXPECT_EQ("bar", message.data["foo"]); + EXPECT_EQ("bash", message.data["bosh"]); + EXPECT_EQ(1234, message.sent_time); + EXPECT_EQ("high", message.priority); + EXPECT_EQ("normal", message.original_priority); + EXPECT_EQ(10, message.time_to_live); + EXPECT_EQ("error0", message.error); + EXPECT_EQ("an error description", message.error_description); + EXPECT_EQ(true, message.notification_opened); + EXPECT_EQ("http://alink.com", message.link); + Notification* notification = message.notification; + EXPECT_NE(nullptr, notification); + EXPECT_EQ("title", notification->title); + EXPECT_EQ("body", notification->body); + EXPECT_EQ("icon", notification->icon); + EXPECT_EQ("sound", notification->sound); + EXPECT_EQ("click_action", notification->click_action); + EXPECT_EQ("body_loc_key", notification->body_loc_key); + EXPECT_EQ(2, notification->body_loc_args.size()); + EXPECT_EQ("1", notification->body_loc_args[0]); + EXPECT_EQ("2", notification->body_loc_args[1]); + EXPECT_EQ("title_loc_key", notification->title_loc_key); + EXPECT_EQ(2, notification->title_loc_args.size()); + EXPECT_EQ("3", notification->title_loc_args[0]); + EXPECT_EQ("4", notification->title_loc_args[1]); + AndroidNotificationParams* android = message.notification->android; + EXPECT_NE(nullptr, android); + EXPECT_EQ("android_channel_id", android->channel_id); +} + +// Try to read from a buffer with a corrupt flatbuffer +TEST_F(MessageReaderTest, ReadFromBufferCorruptFlatbuffer) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedTokenReceived, + CreateSerializedTokenReceived(fbb, fbb.CreateString("clobberme")) + .Union())); + std::string buffer; + AppendFlatBufferToString(&buffer, fbb); + for (size_t i = 0; i < fbb.GetSize(); ++i) { + buffer[sizeof(int32_t) + i] = 0xef; + } + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Try reading from a buffer with an invalid event type. +TEST_F(MessageReaderTest, ReadFromBufferInvalidEventType) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, + CreateSerializedEvent( + fbb, + static_cast( + SerializedEventUnion_MAX + 1), + CreateSerializedTokenReceived(fbb, fbb.CreateString("ignoreme")) + .Union())); + std::string buffer; + AppendFlatBufferToString(&buffer, fbb); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +} // namespace internal +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/android/cpp/messaging_test_util.cc b/messaging/tests/android/cpp/messaging_test_util.cc new file mode 100644 index 0000000000..22af5f12f2 --- /dev/null +++ b/messaging/tests/android/cpp/messaging_test_util.cc @@ -0,0 +1,277 @@ +// Copyright 2017 Google LLC +// +// 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 "messaging/tests/messaging_test_util.h" + +#include +#include +#include +#include + +#include "app/src/util.h" +#include "app/src/util_android.h" +#include "messaging/messaging_generated.h" +#include "messaging/src/android/cpp/messaging_internal.h" +#include "messaging/src/include/firebase/messaging.h" +#include "testing/run_all_tests.h" +#include "flatbuffers/util.h" + +using ::com::google::firebase::messaging::cpp::CreateDataPair; +using ::com::google::firebase::messaging::cpp::CreateSerializedEvent; +using ::com::google::firebase::messaging::cpp::CreateSerializedTokenReceived; +using ::com::google::firebase::messaging::cpp::DataPair; +using ::com::google::firebase::messaging::cpp::SerializedEventUnion; +using ::com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedMessage; +using ::com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedTokenReceived; +using ::com::google::firebase::messaging::cpp::SerializedMessageBuilder; +using ::com::google::firebase::messaging::cpp::SerializedNotification; +using ::com::google::firebase::messaging::cpp::SerializedNotificationBuilder; + +namespace firebase { +namespace messaging { + +static std::string* g_local_storage_file_path; +static std::string* g_lockfile_path; + +// Lock the file referenced by g_lockfile_path. +class TestMessageLockFileLocker : private FileLocker { + public: + TestMessageLockFileLocker() : FileLocker(g_lockfile_path->c_str()) {} + ~TestMessageLockFileLocker() {} +}; + +void InitializeMessagingTest() { + JNIEnv* env = firebase::testing::cppsdk::GetTestJniEnv(); + jobject activity = firebase::testing::cppsdk::GetTestActivity(); + jobject file = env->CallObjectMethod( + activity, util::context::GetMethodId(util::context::kGetFilesDir)); + assert(env->ExceptionCheck() == false); + jstring path_jstring = reinterpret_cast(env->CallObjectMethod( + file, util::file::GetMethodId(util::file::kGetPath))); + assert(env->ExceptionCheck() == false); + std::string local_storage_dir = util::JniStringToString(env, path_jstring); + env->DeleteLocalRef(file); + g_lockfile_path = new std::string(local_storage_dir + "/" + kLockfile); + g_local_storage_file_path = + new std::string(local_storage_dir + "/" + kStorageFile); +} + +void TerminateMessagingTest() { + delete g_lockfile_path; + g_lockfile_path = nullptr; + delete g_local_storage_file_path; + g_local_storage_file_path = nullptr; +} + +static void WriteBuffer(const ::flatbuffers::FlatBufferBuilder& builder) { + TestMessageLockFileLocker file_lock; + FILE* data_file = fopen(g_local_storage_file_path->c_str(), "a"); + int size = builder.GetSize(); + fwrite(&size, sizeof(size), 1, data_file); + fwrite(builder.GetBufferPointer(), size, 1, data_file); + fclose(data_file); +} + +void OnTokenReceived(const char* tokenstr) { + flatbuffers::FlatBufferBuilder builder; + auto token = builder.CreateString(tokenstr); + auto tokenreceived = CreateSerializedTokenReceived(builder, token); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedTokenReceived, + tokenreceived.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnDeletedMessages() { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(""); + auto message_id = builder.CreateString(""); + auto message_type = builder.CreateString("deleted_messages"); + auto error = builder.CreateString(""); + auto link = builder.CreateString(""); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_message_id(message_id); + message_builder.add_message_type(message_type); + message_builder.add_error(error); + message_builder.add_notification_opened(false); + message_builder.add_link(link); + auto message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnMessageReceived(const Message& message) { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(message.from); + auto to = builder.CreateString(message.to); + auto message_id = builder.CreateString(message.message_id); + auto message_type = builder.CreateString(message.message_type); + auto error = builder.CreateString(message.error); + auto priority = builder.CreateString(message.priority); + auto original_priority = builder.CreateString(message.original_priority); + auto collapse_key = builder.CreateString(message.collapse_key); + + std::vector> data_pair_vector; + for (auto const& entry : message.data) { + auto key = builder.CreateString(entry.first); + auto value = builder.CreateString(entry.second); + auto data_pair = CreateDataPair(builder, key, value); + data_pair_vector.push_back(data_pair); + } + auto data = builder.CreateVector(data_pair_vector); + ::flatbuffers::Offset notification; + if (message.notification) { + auto title = builder.CreateString(message.notification->title); + auto body = builder.CreateString(message.notification->body); + auto icon = builder.CreateString(message.notification->icon); + auto sound = builder.CreateString(message.notification->sound); + auto badge = builder.CreateString(message.notification->badge); + auto tag = builder.CreateString(message.notification->tag); + auto color = builder.CreateString(message.notification->color); + auto click_action = + builder.CreateString(message.notification->click_action); + auto body_localization_key = + builder.CreateString(message.notification->body_loc_key); + + std::vector> + body_localization_args_vector; + for (auto const& value : message.notification->body_loc_args) { + auto body_localization_item = builder.CreateString(value); + body_localization_args_vector.push_back(body_localization_item); + } + auto body_localization_args = + builder.CreateVector(body_localization_args_vector); + + auto title_localization_key = + builder.CreateString(message.notification->title_loc_key); + + std::vector> + title_localization_args_vector; + for (auto const& value : message.notification->title_loc_args) { + auto title_localization_item = builder.CreateString(value); + title_localization_args_vector.push_back(title_localization_item); + } + auto title_localization_args = + builder.CreateVector(title_localization_args_vector); + auto android_channel_id = + message.notification->android + ? builder.CreateString(message.notification->android->channel_id) + : 0; + + SerializedNotificationBuilder notification_builder(builder); + notification_builder.add_title(title); + notification_builder.add_body(body); + notification_builder.add_icon(icon); + notification_builder.add_sound(sound); + notification_builder.add_badge(badge); + notification_builder.add_tag(tag); + notification_builder.add_color(color); + notification_builder.add_click_action(click_action); + notification_builder.add_body_loc_key(body_localization_key); + notification_builder.add_body_loc_args(body_localization_args); + notification_builder.add_title_loc_key(title_localization_key); + notification_builder.add_title_loc_args(title_localization_args); + if (message.notification->android) { + notification_builder.add_android_channel_id(android_channel_id); + } + notification = notification_builder.Finish(); + } + auto link = builder.CreateString(message.link); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_to(to); + message_builder.add_message_id(message_id); + message_builder.add_message_type(message_type); + message_builder.add_priority(priority); + message_builder.add_original_priority(original_priority); + message_builder.add_sent_time(message.sent_time); + message_builder.add_time_to_live(message.time_to_live); + message_builder.add_collapse_key(collapse_key); + if (!notification.IsNull()) { + message_builder.add_notification(notification); + } + message_builder.add_error(error); + message_builder.add_notification_opened(message.notification_opened); + message_builder.add_link(link); + message_builder.add_data(data); + auto serialized_message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + serialized_message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnMessageSent(const char* message_id) { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(""); + auto message_id_offset = builder.CreateString(message_id); + auto message_type = builder.CreateString("send_event"); + auto error = builder.CreateString(""); + auto link = builder.CreateString(""); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_message_id(message_id_offset); + message_builder.add_message_type(message_type); + message_builder.add_error(error); + message_builder.add_notification_opened(false); + message_builder.add_link(link); + auto message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnMessageSentError(const char* message_id, const char* error) { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(""); + auto message_id_offset = builder.CreateString(message_id); + auto message_type = builder.CreateString("send_error"); + auto error_offset = builder.CreateString(error); + auto link = builder.CreateString(""); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_message_id(message_id_offset); + message_builder.add_message_type(message_type); + message_builder.add_error(error_offset); + message_builder.add_notification_opened(false); + message_builder.add_link(link); + auto message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void SleepMessagingTest(double seconds) { + sleep(static_cast(seconds + 0.5)); +} + +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/ios/messaging_test_util.mm b/messaging/tests/ios/messaging_test_util.mm new file mode 100644 index 0000000000..106381b769 --- /dev/null +++ b/messaging/tests/ios/messaging_test_util.mm @@ -0,0 +1,99 @@ +// Copyright 2017 Google LLC +// +// 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 "messaging/tests/messaging_test_util.h" + +#import + +#include "app/src/log.h" +#include "messaging/src/include/firebase/messaging.h" + +#import "messaging/src/ios/fake/FIRMessaging.h" + +namespace firebase { +namespace messaging { + +// Message keys. +static NSString *const kFrom = @"from"; +static NSString *const kTo = @"to"; +static NSString *const kCollapseKey = @"collapse_key"; +static NSString *const kMessageID = @"gcm.message_id"; +static NSString *const kMessageType = @"message_type"; +static NSString *const kPriority = @"priority"; +static NSString *const kTimeToLive = @"time_to_live"; +static NSString *const kError = @"error"; +static NSString *const kErrorDescription = @"error_description"; + +// Notification keys. +static NSString *const kTitle = @"title"; +static NSString *const kBody = @"body"; +static NSString *const kSound = @"sound"; +static NSString *const kBadge = @"badge"; + +// Dual purpose body text or data dictionary. +static NSString *const kAlert = @"alert"; + + +void InitializeMessagingTest() {} + +void TerminateMessagingTest() { + [FIRMessaging messaging].FCMToken = nil; +} + +void OnTokenReceived(const char* tokenstr) { + [FIRMessaging messaging].FCMToken = @(tokenstr); + [[FIRMessaging messaging].delegate messaging:[FIRMessaging messaging] + didReceiveRegistrationToken:@(tokenstr)]; +} + +void SleepMessagingTest(double seconds) { + // We want the main loop to process messages while we wait. + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:seconds]]; +} + +void OnMessageReceived(const Message& message) { + NSMutableDictionary* userData = [NSMutableDictionary dictionary]; + userData[kMessageID] = @(message.message_id.c_str()); + userData[kTo] = @(message.to.c_str()); + userData[kFrom] = @(message.from.c_str()); + userData[kCollapseKey] = @(message.collapse_key.c_str()); + userData[kMessageType] = @(message.message_type.c_str()); + userData[kPriority] = @(message.priority.c_str()); + userData[kTimeToLive] = @(message.time_to_live); + userData[kError] = @(message.error.c_str()); + userData[kErrorDescription] = @(message.error_description.c_str()); + for (const auto& entry : message.data) { + userData[@(entry.first.c_str())] = @(entry.second.c_str()); + } + + if (message.notification) { + NSMutableDictionary* alert = [NSMutableDictionary dictionary]; + alert[kTitle] = @(message.notification->title.c_str()); + alert[kBody] = @(message.notification->body.c_str()); + NSMutableDictionary* aps = [NSMutableDictionary dictionary]; + aps[kSound] = @(message.notification->sound.c_str()); + aps[kBadge] = @(message.notification->badge.c_str()); + aps[kAlert] = alert; + userData[@"aps"] = aps; + } + [[[UIApplication sharedApplication] delegate] application:[UIApplication sharedApplication] + didReceiveRemoteNotification:userData]; +} + +void OnMessageSent(const char* message_id) {} + +void OnMessageSentError(const char* message_id, const char* error) {} + +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/messaging_test.cc b/messaging/tests/messaging_test.cc new file mode 100644 index 0000000000..3da4ed9df4 --- /dev/null +++ b/messaging/tests/messaging_test.cc @@ -0,0 +1,380 @@ +// Copyright 2017 Google LLC +// +// 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. + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "messaging/src/include/firebase/messaging.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#if defined(__APPLE__) +#include +#endif // defined(__APPLE_) + +#include "app/src/util.h" +#include "messaging/tests/messaging_test_util.h" +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::StrEq; + +namespace firebase { +namespace messaging { + +class MessagingTestListener : public Listener { + public: + void OnMessage(const Message& message) override; + void OnTokenReceived(const char* token) override; + + const Message& GetMessage() const { + return message_; + } + + const std::string& GetToken() const { + return token_; + } + + int GetOnTokenReceivedCount() const { + return on_token_received_count_; + } + + int GetOnMessageReceivedCount() const { + return on_message_received_count_; + } + + private: + Message message_; + std::string token_; + int on_token_received_count_ = 0; + int on_message_received_count_ = 0; +}; + +class MessagingTest : public ::testing::Test { + protected: + void SetUp() override { + // Cache the local storage file and lockfile. + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + reporter_.reset(); + + firebase_app_ = testing::CreateApp(); + InitializeMessagingTest(); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + } + + void TearDown() override { + firebase::testing::cppsdk::ConfigReset(); + Terminate(); + TerminateMessagingTest(); + delete firebase_app_; + firebase_app_ = nullptr; + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + void AddExpectationAndroid( + const char* fake, std::initializer_list args) { + reporter_.addExpectation( + fake, "", firebase::testing::cppsdk::kAndroid, args); + } + + void AddExpectationApple( + const char* fake, std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kIos, args); + } + + App* firebase_app_ = nullptr; + MessagingTestListener listener_; + firebase::testing::cppsdk::Reporter reporter_; +}; + +void MessagingTestListener::OnMessage(const Message& message) { + message_ = message; + on_message_received_count_++; +} + +void MessagingTestListener::OnTokenReceived(const char* token) { + token_ = token; + on_token_received_count_++; +} + +// Tests only run on Android for now. +TEST_F(MessagingTest, TestInitializeTwice) { + MessagingTestListener listener; + EXPECT_EQ(Initialize(*firebase_app_, &listener), kInitResultSuccess); +} + +// The order of these matter because of the global flag +// g_registration_token_received +TEST_F(MessagingTest, TestSubscribeNoRegistration) { + Subscribe("topic"); + SleepMessagingTest(1); + // Android should cache the call, iOS will subscribe right away. + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"topic"}); +} + +// TODO(westarle): break up this test when subscriber queuing is testable. +TEST_F(MessagingTest, TestSubscribeBeforeRegistration) { + Subscribe("$invalid"); + Subscribe("subscribe_topic1"); + Subscribe("subscribe_topic2"); + Unsubscribe("$invalid"); + Unsubscribe("unsubscribe_topic1"); + Unsubscribe("unsubscribe_topic2"); + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"$invalid", "subscribe_topic1", "subscribe_topic2"}); + AddExpectationApple("-[FIRMessaging unsubscribeFromTopic:completion:]", + {"$invalid", "unsubscribe_topic1", "unsubscribe_topic2"}); + + // No requests to Java API yet, iOS should go ahead and forward. + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + + OnTokenReceived("my_token"); + SleepMessagingTest(1); + AddExpectationAndroid("FirebaseMessaging.subscribeToTopic", + {"$invalid", "subscribe_topic1", "subscribe_topic2"}); + + AddExpectationAndroid("FirebaseMessaging.unsubscribeFromTopic", + {"$invalid", "unsubscribe_topic1", "unsubscribe_topic2"}); +} + +TEST_F(MessagingTest, TestSubscribeAfterRegistration) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Subscribe("topic"); + + AddExpectationAndroid("FirebaseMessaging.subscribeToTopic", {"topic"}); + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"topic"}); +} + +TEST_F(MessagingTest, TestUnsubscribeAfterRegistration) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Unsubscribe("topic"); + AddExpectationAndroid("FirebaseMessaging.unsubscribeFromTopic", {"topic"}); + AddExpectationApple("-[FIRMessaging unsubscribeFromTopic:completion:]", + {"topic"}); +} + +TEST_F(MessagingTest, TestTokenReceived) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); +} + +TEST_F(MessagingTest, TestTokenReceivedBeforeInitialize) { + Terminate(); + OnTokenReceived("my_token"); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); +} + +TEST_F(MessagingTest, TestTwoTokensReceivedBeforeInitialize) { + Terminate(); + OnTokenReceived("my_token1"); + OnTokenReceived("my_token2"); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token2")); +} + +TEST_F(MessagingTest, TestTwoTokensReceivedAfterInitialize) { + OnTokenReceived("my_token1"); + OnTokenReceived("my_token2"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token2")); + EXPECT_EQ(listener_.GetOnTokenReceivedCount(), 2); +} + +TEST_F(MessagingTest, TestTwoIdenticalTokensReceived) { + OnTokenReceived("my_token"); + OnTokenReceived("my_token"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); + EXPECT_EQ(listener_.GetOnTokenReceivedCount(), 1); +} + +TEST_F(MessagingTest, TestTokenReceivedNoListener) { + Terminate(); + EXPECT_EQ(Initialize(*firebase_app_, nullptr), kInitResultSuccess); + OnTokenReceived("my_token"); + SleepMessagingTest(1); + SetListener(&listener_); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); + EXPECT_EQ(listener_.GetOnTokenReceivedCount(), 1); +} + +TEST_F(MessagingTest, TestSubscribeInvalidTopic) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Subscribe("$invalid"); + AddExpectationAndroid("FirebaseMessaging.subscribeToTopic", {"$invalid"}); + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"$invalid"}); +} + +TEST_F(MessagingTest, TestUnsubscribeInvalidTopic) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Unsubscribe("$invalid"); + AddExpectationAndroid("FirebaseMessaging.unsubscribeFromTopic", {"$invalid"}); + AddExpectationApple("-[FIRMessaging unsubscribeFromTopic:completion:]", + {"$invalid"}); +} + +TEST_F(MessagingTest, TestDataMessageReceived) { + Message message; + message.from = "my_from"; + message.data["my_key"] = "my_value"; + OnMessageReceived(message); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().from, StrEq("my_from")); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("")); + EXPECT_THAT(listener_.GetMessage().error, StrEq("")); + EXPECT_THAT(listener_.GetMessage().data.at("my_key"), StrEq("my_value")); +} + +TEST_F(MessagingTest, TestNotificationReceived) { + Message send_message; + send_message.from = "my_from"; + send_message.to = "my_to"; + send_message.message_id = "id"; + send_message.message_type = "type"; + send_message.error = ""; + send_message.data["my_key"] = "my_value"; + send_message.notification = new Notification; + send_message.notification->title = "my_title"; + send_message.notification->body = "my_body"; + send_message.notification->icon = "my_icon"; + send_message.notification->sound = "my_sound"; + send_message.notification->tag = "my_tag"; + send_message.notification->color = "my_color"; + send_message.notification->click_action = "my_click_action"; + send_message.notification->body_loc_key = "my_body_localization_key"; + send_message.notification->body_loc_args.push_back( + "my_body_localization_item"); + send_message.notification->title_loc_key = "my_title_localization_key"; + send_message.notification->title_loc_args.push_back( + "my_title_localization_item"); + send_message.notification_opened = true; + send_message.notification->android = new AndroidNotificationParams; + send_message.notification->android->channel_id = "my_android_channel_id"; + send_message.collapse_key = "my_collapse_key"; + send_message.priority = "my_priority"; + send_message.original_priority = "normal"; + send_message.time_to_live = 1234; + send_message.sent_time = 5678; + OnMessageReceived(send_message); + SleepMessagingTest(1); + const Message& message = listener_.GetMessage(); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(message.from, StrEq("my_from")); + EXPECT_THAT(message.to, StrEq("my_to")); + EXPECT_THAT(message.message_id, StrEq("id")); + EXPECT_THAT(message.message_type, StrEq("type")); + EXPECT_THAT(message.error, StrEq("")); + EXPECT_THAT(message.data.at("my_key"), StrEq("my_value")); + EXPECT_TRUE(message.notification_opened); + EXPECT_THAT(message.notification->title, StrEq("my_title")); + EXPECT_THAT(message.notification->body, StrEq("my_body")); + EXPECT_THAT(message.notification->sound, StrEq("my_sound")); + EXPECT_THAT(message.collapse_key, StrEq("my_collapse_key")); + EXPECT_THAT(message.priority, StrEq("my_priority")); + EXPECT_EQ(message.time_to_live, 1234); +#if !TARGET_OS_IPHONE + EXPECT_THAT(message.original_priority, StrEq("normal")); + EXPECT_EQ(message.sent_time, 5678); +#endif // !TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_THAT(message.notification->icon, StrEq("my_icon")); + EXPECT_THAT(message.notification->tag, StrEq("my_tag")); + EXPECT_THAT(message.notification->color, StrEq("my_color")); + EXPECT_THAT(message.notification->click_action, StrEq("my_click_action")); + EXPECT_THAT( + message.notification->body_loc_key, StrEq("my_body_localization_key")); + EXPECT_THAT(message.notification->body_loc_args[0], + StrEq("my_body_localization_item")); + EXPECT_THAT( + message.notification->title_loc_key, StrEq("my_title_localization_key")); + EXPECT_THAT(message.notification->title_loc_args[0], + StrEq("my_title_localization_item")); + EXPECT_THAT(message.notification->android->channel_id, + StrEq("my_android_channel_id")); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + +TEST_F(MessagingTest, TestOnDeletedMessages) { + OnDeletedMessages(); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().from, StrEq("")); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("deleted_messages")); + EXPECT_THAT(listener_.GetMessage().error, StrEq("")); +} + +TEST_F(MessagingTest, TestSendMessage) { + Message message; + message.to = "my_to"; + message.message_id = "my_message_id"; + message.data["my_key"] = "my_value"; + message.time_to_live = 1000; + Send(message); + AddExpectationAndroid("FirebaseMessaging.send", + {"my_to", "{my_key=my_value}", "my_message_id", "my_from", "1000"}); +} + +TEST_F(MessagingTest, TestOnMessageSent) { + OnMessageSent("my_message_id"); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("my_message_id")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("send_event")); +} + +TEST_F(MessagingTest, TestOnSendError) { + OnMessageSentError("my_message_id", "my_exception"); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("my_message_id")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("send_error")); + EXPECT_THAT(listener_.GetMessage().error, StrEq("my_exception")); +} + + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/messaging_test_util.h b/messaging/tests/messaging_test_util.h new file mode 100644 index 0000000000..b027cd8ef7 --- /dev/null +++ b/messaging/tests/messaging_test_util.h @@ -0,0 +1,51 @@ +// Copyright 2017 Google LLC +// +// 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. + +// This file contains utility methods used by messaging tests where the +// implementation diverges across platforms. +#ifndef FIREBASE_MESSAGING_CLIENT_CPP_TESTS_MESSAGING_TEST_UTIL_H_ +#define FIREBASE_MESSAGING_CLIENT_CPP_TESTS_MESSAGING_TEST_UTIL_H_ + +namespace firebase { +namespace messaging { + +struct Message; + +// Sleep this thread for some amount of time and process important messages. +// e.g. let the Android messaging implementation wake up the thread watching +// the file. +void SleepMessagingTest(double seconds); + +// Once-per-test platform specific initialization (e.g. the Android test +// implementation will initialize filenames by JNI calls. +void InitializeMessagingTest(); + +// Once-per-test platform-specific teardown. +void TerminateMessagingTest(); + +// Simulate a token received/refresh event from the OS-level implementation. +void OnTokenReceived(const char* tokenstr); + +void OnDeletedMessages(); + +void OnMessageReceived(const Message& message); + +void OnMessageSent(const char* message_id); + +void OnMessageSentError(const char* message_id, const char* error); + +} // namespace messaging +} // namespace firebase + +#endif // FIREBASE_MESSAGING_CLIENT_CPP_TESTS_MESSAGING_TEST_UTIL_H_ diff --git a/remote_config/src/desktop/rest_fake.cc b/remote_config/src/desktop/rest_fake.cc new file mode 100644 index 0000000000..addb8a227f --- /dev/null +++ b/remote_config/src/desktop/rest_fake.cc @@ -0,0 +1,73 @@ +// Copyright 2017 Google LLC +// +// 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 "remote_config/src/desktop/rest.h" + +#include // NOLINT +#include +#include + +#include "firebase/app.h" +#include "remote_config/src/desktop/config_data.h" +#include "remote_config/src/desktop/rest_nanopb_encode.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +// Stub REST implementation. +// The purpose of this class is to hold content and not actually do anything +// with it when the normal API calls happen. +RemoteConfigREST::RemoteConfigREST(const firebase::AppOptions& app_options, + const LayeredConfigs& configs, + uint64_t cache_expiration_in_seconds) + : app_package_name_(app_options.app_id()), + app_gmp_project_id_(app_options.project_id()), + configs_(configs), + cache_expiration_in_seconds_(cache_expiration_in_seconds), + fetch_future_sem_(0) { + configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "value"}}}}), 1000000); + + configs_.metadata.set_info( + ConfigInfo{0, kLastFetchStatusSuccess, kFetchFailureReasonError, 0}); + configs_.metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace", "digest"}})); +} + +RemoteConfigREST::~RemoteConfigREST() {} + +void RemoteConfigREST::Fetch(const App& app) {} + +void RemoteConfigREST::SetupRestRequest() {} + +ConfigFetchRequest RemoteConfigREST::GetFetchRequestData() { + return ConfigFetchRequest(); +} + +void RemoteConfigREST::GetPackageData(PackageData* package_data) {} + +void RemoteConfigREST::ParseRestResponse() {} + +void RemoteConfigREST::ParseProtoResponse(const std::string& proto_str) {} + +void RemoteConfigREST::FetchSuccess(LastFetchStatus status) {} + +void RemoteConfigREST::FetchFailure(FetchFailureReason reason) {} + +uint64_t RemoteConfigREST::MillisecondsSinceEpoch() { return 0; } + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java new file mode 100644 index 0000000000..9e25f0857b --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -0,0 +1,269 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** Fake FirebaseRemoteConfig */ +public class FirebaseRemoteConfig { + + private static final String FN_ACTIVATE_FETCHED = "FirebaseRemoteConfig.activateFetched"; + private static final String FN_SET_DEFAULTS = "FirebaseRemoteConfig.setDefaults"; + private static final String FN_SET_CONFIG_SETTINGS = "FirebaseRemoteConfig.setConfigSettings"; + private static final String FN_GET_LONG = "FirebaseRemoteConfig.getLong"; + private static final String FN_GET_BYTE_ARRAY = "FirebaseRemoteConfig.getByteArray"; + private static final String FN_GET_STRING = "FirebaseRemoteConfig.getString"; + private static final String FN_GET_BOOLEAN = "FirebaseRemoteConfig.getBoolean"; + private static final String FN_GET_DOUBLE = "FirebaseRemoteConfig.getDouble"; + private static final String FN_GET_VALUE = "FirebaseRemoteConfig.getValue"; + private static final String FN_GET_INFO = "FirebaseRemoteConfig.getInfo"; + private static final String FN_GET_KEYS_BY_PREFIX = "FirebaseRemoteConfig.getKeysByPrefix"; + private static final String FN_GET_ALL = "FirebaseRemoteConfig.getAll"; + private static final String FN_FETCH = "FirebaseRemoteConfig.fetch"; + private static final String FN_ENSURE_INITIALIZED = "FirebaseRemoteConfig.ensureInitialized"; + private static final String FN_ACTIVATE = "FirebaseRemoteConfig.activate"; + private static final String FN_FETCH_AND_ACTIVATE = "FirebaseRemoteConfig.fetchAndActivate"; + private static final String FN_SET_DEFAULTS_ASYNC = "FirebaseRemoteConfig.setDefaultsAsync"; + private static final String FN_SET_CONFIG_SETTINGS_ASYNC = + "FirebaseRemoteConfig.setConfigSettingsAsync"; + + FirebaseRemoteConfig() {} + + public static FirebaseRemoteConfig getInstance() { + return new FirebaseRemoteConfig(); + } + + public boolean activateFetched() { + ConfigRow row = ConfigAndroid.get(FN_ACTIVATE_FETCHED); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult(FN_ACTIVATE_FETCHED, String.valueOf(result)); + return result; + } + + public void setDefaults(int resourceId) { + FakeReporter.addReport(FN_SET_DEFAULTS, Integer.toString(resourceId)); + } + + public void setDefaults(int resourceId, String namespace) { + FakeReporter.addReport(FN_SET_DEFAULTS, Integer.toString(resourceId), namespace); + } + + public void setDefaults(Map defaults) { + Map sorted = new TreeMap<>(defaults); + FakeReporter.addReport(FN_SET_DEFAULTS, sorted.toString()); + } + + public void setDefaults(Map defaults, String namespace) { + Map sorted = new TreeMap<>(defaults); + FakeReporter.addReport(FN_SET_DEFAULTS, sorted.toString(), namespace); + } + + public void setConfigSettings(FirebaseRemoteConfigSettings settings) { + FakeReporter.addReport(FN_SET_CONFIG_SETTINGS); + } + + public long getLong(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_LONG); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult(FN_GET_LONG, Long.toString(result), key); + return result; + } + + public long getLong(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_LONG); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult(FN_GET_LONG, Long.toString(result), key, namespace); + return result; + } + + public byte[] getByteArray(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_BYTE_ARRAY); + byte[] result = row.returnvalue().tstring().getBytes(UTF_8); + FakeReporter.addReportWithResult(FN_GET_BYTE_ARRAY, new String(result), key); + return result; + } + + public byte[] getByteArray(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_BYTE_ARRAY); + byte[] result = row.returnvalue().tstring().getBytes(UTF_8); + FakeReporter.addReportWithResult(FN_GET_BYTE_ARRAY, new String(result), key, namespace); + return result; + } + + public String getString(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_STRING); + String result = row.returnvalue().tstring(); + FakeReporter.addReportWithResult(FN_GET_STRING, result, key); + return result; + } + + public String getString(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_STRING); + String result = row.returnvalue().tstring(); + FakeReporter.addReportWithResult(FN_GET_STRING, result, key, namespace); + return result; + } + + public boolean getBoolean(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_BOOLEAN); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult(FN_GET_BOOLEAN, String.valueOf(result), key); + return result; + } + + public boolean getBoolean(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_BOOLEAN); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult(FN_GET_BOOLEAN, String.valueOf(result), key, namespace); + return result; + } + + public double getDouble(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_DOUBLE); + double result = row.returnvalue().tdouble(); + FakeReporter.addReportWithResult(FN_GET_DOUBLE, String.format("%.3f", result), key); + return result; + } + + public double getDouble(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_DOUBLE); + double result = row.returnvalue().tdouble(); + FakeReporter.addReportWithResult(FN_GET_DOUBLE, String.format("%.3f", result), key, namespace); + return result; + } + + public FirebaseRemoteConfigValue getValue(String key) { + FakeReporter.addReport(FN_GET_VALUE, key); + return new FirebaseRemoteConfigValue(); + } + + public FirebaseRemoteConfigValue getValue(String key, String namespace) { + FakeReporter.addReport(FN_GET_VALUE, key, namespace); + return new FirebaseRemoteConfigValue(); + } + + public FirebaseRemoteConfigInfo getInfo() { + FakeReporter.addReport(FN_GET_INFO); + return new FirebaseRemoteConfigInfo(); + } + + public Set getKeysByPrefix(String prefix) { + ConfigRow row = ConfigAndroid.get(FN_GET_KEYS_BY_PREFIX); + Set result = new TreeSet<>(stringToStringList(row.returnvalue().tstring())); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfig.getKeysByPrefix", result.toString(), prefix); + return result; + } + + public Set getKeysByPrefix(String prefix, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_KEYS_BY_PREFIX); + Set result = new TreeSet<>(stringToStringList(row.returnvalue().tstring())); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfig.getKeysByPrefix", result.toString(), prefix, namespace); + return result; + } + + public Map getAll() { + FakeReporter.addReport(FN_GET_ALL); + return new HashMap<>(); + } + + private static Task voidHelper(String configKey) { + Task result = Task.forResult(configKey, null); + TickerAndroid.register(result); + return result; + } + + public Task fetch() { + FakeReporter.addReport(FN_FETCH); + return voidHelper(FN_FETCH); + } + + public Task fetch(long cacheExpirationSeconds) { + FakeReporter.addReport(FN_FETCH, Long.toString(cacheExpirationSeconds)); + return voidHelper(FN_FETCH); + } + + private static Task eIHelper(String configKey) { + Task result = + Task.forResult(configKey, new FirebaseRemoteConfigInfo()); + TickerAndroid.register(result); + return result; + } + + public Task ensureInitialized() { + FakeReporter.addReport(FN_ENSURE_INITIALIZED); + return eIHelper(FN_ENSURE_INITIALIZED); + } + + private static Task booleanHelper(String configKey) { + Task result = Task.forResult(configKey, Boolean.TRUE); + TickerAndroid.register(result); + return result; + } + + public Task activate() { + FakeReporter.addReport(FN_ACTIVATE); + return booleanHelper(FN_ACTIVATE); + } + + public Task fetchAndActivate() { + FakeReporter.addReport(FN_FETCH_AND_ACTIVATE); + return booleanHelper(FN_FETCH_AND_ACTIVATE); + } + + public Task setDefaultsAsync(int resourceId) { + FakeReporter.addReport(FN_SET_DEFAULTS_ASYNC, Integer.toString(resourceId)); + return voidHelper(FN_SET_DEFAULTS_ASYNC); + } + + public Task setDefaultsAsync(Map defaults) { + Map sorted = new TreeMap<>(defaults); + FakeReporter.addReport(FN_SET_DEFAULTS_ASYNC, sorted.toString()); + return voidHelper(FN_SET_DEFAULTS_ASYNC); + } + + public Task setConfigSettingsAsync(FirebaseRemoteConfigSettings settings) { + FakeReporter.addReport(FN_SET_CONFIG_SETTINGS_ASYNC); + return voidHelper(FN_SET_CONFIG_SETTINGS_ASYNC); + } + + private static List stringToStringList(String s) { + s = s.substring(1, s.length() - 1); + if (s.length() == 0) { + return new ArrayList(); + } + String[] arr = s.split(","); + for (int i = 0; i < arr.length; i++) { + arr[i] = arr[i].trim(); + } + return Arrays.asList(arr); + } + +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java new file mode 100644 index 0000000000..8e72f08587 --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java @@ -0,0 +1,24 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +/** Fake FirebaseRemoteConfigFetchThrottledException */ +public class FirebaseRemoteConfigFetchThrottledException { + + public long getThrottleEndTimeMillis() { + return 0; + } + +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java new file mode 100644 index 0000000000..5c61295168 --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java @@ -0,0 +1,44 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; + +/** Fake FirebaseRemoteConfigInfo */ +public class FirebaseRemoteConfigInfo { + + public long getFetchTimeMillis() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigInfo.getFetchTimeMillis"); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigInfo.getFetchTimeMillis", Long.toString(result)); + return result; + } + + public int getLastFetchStatus() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigInfo.getLastFetchStatus"); + int result = row.returnvalue().tint(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigInfo.getLastFetchStatus", Integer.toString(result)); + return result; + } + + public FirebaseRemoteConfigSettings getConfigSettings() { + FakeReporter.addReport("FirebaseRemoteConfigInfo.getConfigSettings"); + return new FirebaseRemoteConfigSettings(); + } +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java new file mode 100644 index 0000000000..aa4c1d229d --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java @@ -0,0 +1,45 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; + +/** Fake FirebaseRemoteConfigSettings */ +public class FirebaseRemoteConfigSettings { + + public boolean isDeveloperModeEnabled() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigSettings.isDeveloperModeEnabled"); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigSettings.isDeveloperModeEnabled", String.valueOf(result)); + return result; + } + + /** Fake Builder */ + public static class Builder { + public Builder setDeveloperModeEnabled(boolean enabled) { + FakeReporter.addReport( + "FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled", String.valueOf(enabled)); + return this; + } + + public FirebaseRemoteConfigSettings build() { + FakeReporter.addReport("FirebaseRemoteConfigSettings.Builder.build"); + return new FirebaseRemoteConfigSettings(); + } + } +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java new file mode 100644 index 0000000000..1095739e48 --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java @@ -0,0 +1,72 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; + +/** Fake FirebaseRemoteConfigValue */ +public class FirebaseRemoteConfigValue { + + public FirebaseRemoteConfigValue() {} + + public long asLong() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asLong"); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asLong", Long.toString(result)); + return result; + } + + public double asDouble() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asDouble"); + double result = row.returnvalue().tdouble(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigValue.asDouble", String.format("%.3f", result)); + return result; + } + + public String asString() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asString"); + String result = row.returnvalue().tstring(); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asString", result); + return result; + } + + public byte[] asByteArray() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asByteArray"); + byte[] result = {}; + result = row.returnvalue().tstring().getBytes(UTF_8); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asByteArray", new String(result)); + return result; + } + + public boolean asBoolean() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asBoolean"); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asBoolean", String.valueOf(result)); + return result; + }; + + public int getSource() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.getSource"); + int result = row.returnvalue().tint(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigValue.getSource", Integer.toString(result)); + return result; + } +} diff --git a/remote_config/tests/CMakeLists.txt b/remote_config/tests/CMakeLists.txt new file mode 100644 index 0000000000..6f5b7433db --- /dev/null +++ b/remote_config/tests/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright 2019 Google LLC +# +# 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. + +# TODO: This test is currently Android-only and needs extra java deps. +# TODO: Work out how to make it work for desktop +#[[ +firebase_cpp_cc_test( + firebase_remote_config_test + SOURCES + remote_config_test.cc + DEPENDS + firebase_app_for_testing + firebase_remote_config + firebase_testing +) +]] + +firebase_cpp_cc_test( + firebase_remote_config_desktop_config_data_test + SOURCES + desktop/config_data_test.cc + DEPENDS + firebase_app_for_testing + firebase_remote_config + firebase_testing +) diff --git a/remote_config/tests/desktop/config_data_test.cc b/remote_config/tests/desktop/config_data_test.cc new file mode 100644 index 0000000000..b8792c6b5c --- /dev/null +++ b/remote_config/tests/desktop/config_data_test.cc @@ -0,0 +1,156 @@ +// Copyright 2018 Google LLC +// +// 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 "remote_config/src/desktop/config_data.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +TEST(LayeredConfigsTest, Convertation) { + NamespacedConfigData fetched( + NamespaceKeyValueMap( + {{"namespace1", {{"key1", "value1"}, {"key2", "value2"}}}}), + 1234567); + NamespacedConfigData active( + NamespaceKeyValueMap( + {{"namespace2", {{"key1", "value1"}, {"key2", "value2"}}}}), + 5555555); + NamespacedConfigData defaults( + NamespaceKeyValueMap( + {{"namespace3", {{"key1", "value1"}, {"key2", "value2"}}}}), + 9999999); + + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "0"); + + LayeredConfigs configs(fetched, active, defaults, metadata); + std::string buffer = configs.Serialize(); + LayeredConfigs new_configs; + new_configs.Deserialize(buffer); + + EXPECT_EQ(configs, new_configs); +} + +TEST(NamespacedConfigDataTest, ConversionToFlexbuffer) { + NamespacedConfigData config_data( + NamespaceKeyValueMap( + {{"namespace1", {{"key1", "value1"}, {"key2", "value2"}}}}), + 1234567); + + // Serialize the data to a string + std::string buffer = config_data.Serialize(); + + // Make a new config and deserialize it with the string. + NamespacedConfigData new_config_data; + new_config_data.Deserialize(buffer); + + EXPECT_EQ(config_data, new_config_data); +} + +TEST(NamespacedConfigDataTest, DefaultConstructor) { + NamespacedConfigData holder1; + NamespacedConfigData holder2(NamespaceKeyValueMap(), 0); + EXPECT_EQ(holder1, holder2); +} + +TEST(NamespacedConfigDataTest, SetNamespace) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 0); + EXPECT_EQ(holder.GetValue("key1", "namespace1"), "value1"); + + holder.SetNamespace(std::map({{"key2", "value2"}}), + "namespace1"); + + EXPECT_FALSE(holder.HasValue("key1", "namespace1")); + EXPECT_EQ(holder.GetValue("key2", "namespace1"), "value2"); +} + +TEST(NamespacedConfigDataTest, HasValue) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 0); + EXPECT_TRUE(holder.HasValue("key1", "namespace1")); + EXPECT_FALSE(holder.HasValue("key2", "namespace1")); + EXPECT_FALSE(holder.HasValue("key3", "namespace2")); +} + +TEST(NamespacedConfigDataTest, HasValueEmpty) { + NamespacedConfigData holder(NamespaceKeyValueMap(), 0); + EXPECT_FALSE(holder.HasValue("key1", "namespace1")); + EXPECT_FALSE(holder.HasValue("key2", "namespace1")); + EXPECT_FALSE(holder.HasValue("key1", "namespace2")); + EXPECT_FALSE(holder.HasValue("key3", "namespace3")); +} + +TEST(NamespacedConfigDataTest, GetValue) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 0); + EXPECT_EQ(holder.GetValue("key1", "namespace1"), "value1"); + EXPECT_EQ(holder.GetValue("key2", "namespace1"), ""); + EXPECT_EQ(holder.GetValue("key3", "namespace2"), ""); + EXPECT_EQ(holder.GetValue("key4", "namespace2"), ""); +} + +TEST(NamespacedConfigDataTest, GetValueEmpty) { + NamespacedConfigData holder(NamespaceKeyValueMap(), 0); + EXPECT_EQ(holder.GetValue("key1", "namespace1"), ""); + EXPECT_EQ(holder.GetValue("key2", "namespace2"), ""); +} + +TEST(NamespacedConfigDataTest, GetKeysByPrefix) { + NamespaceKeyValueMap m( + {{"namespace1", + {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}}}); + NamespacedConfigData holder(m, 0); + std::set keys; + holder.GetKeysByPrefix("key", "namespace1", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre("key1", "key2", "key3")); + keys.clear(); + + holder.GetKeysByPrefix("", "namespace1", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre("key1", "key2", "key3")); + keys.clear(); + + holder.GetKeysByPrefix("some_other_key", "namespace1", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre()); + keys.clear(); + + holder.GetKeysByPrefix("some_prefix", "namespace2", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre()); + keys.clear(); +} + +TEST(NamespacedConfigDataTest, GetConfig) { + NamespaceKeyValueMap m( + {{"namespace1", + {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}}}); + NamespacedConfigData holder(m, 1498757224); + EXPECT_EQ(holder.config(), m); +} + +TEST(NamespacedConfigDataTest, GetTimestamp) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 1498757224); + EXPECT_EQ(holder.timestamp(), 1498757224); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/file_manager_test.cc b/remote_config/tests/desktop/file_manager_test.cc new file mode 100644 index 0000000000..591d016c8c --- /dev/null +++ b/remote_config/tests/desktop/file_manager_test.cc @@ -0,0 +1,66 @@ +// Copyright 2017 Google LLC +// +// 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 + +#include "testing/base/public/googletest.h" +#include "gtest/gtest.h" + +#include "file/base/path.h" +#include "remote_config/src/desktop/config_data.h" +#include "remote_config/src/desktop/file_manager.h" +#include "remote_config/src/desktop/metadata.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +TEST(RemoteConfigFileManagerTest, SaveAndLoadSuccess) { + std::string file_path = + file::JoinPath(FLAGS_test_tmpdir, "remote_config_data"); + + RemoteConfigFileManager file_manager(file_path); + NamespacedConfigData fetched( + NamespaceKeyValueMap( + {{"namespace1", {{"key1", "value1"}, {"key2", "value2"}}}}), + 1234567); + NamespacedConfigData active( + NamespaceKeyValueMap( + {{"namespace2", {{"key1", "value1"}, {"key2", "value2"}}}}), + 5555555); + NamespacedConfigData defaults( + NamespaceKeyValueMap( + {{"namespace3", {{"key1", "value1"}, {"key2", "value2"}}}}), + 9999999); + + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "0"); + + LayeredConfigs configs(fetched, active, defaults, metadata); + + EXPECT_TRUE(file_manager.Save(configs)); + + LayeredConfigs new_configs; + EXPECT_TRUE(file_manager.Load(&new_configs)); + EXPECT_EQ(configs, new_configs); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/metadata_test.cc b/remote_config/tests/desktop/metadata_test.cc new file mode 100644 index 0000000000..4de1570a3b --- /dev/null +++ b/remote_config/tests/desktop/metadata_test.cc @@ -0,0 +1,101 @@ +// Copyright 2017 Google LLC +// +// 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 + +#include "gtest/gtest.h" + +#include "remote_config/src/desktop/metadata.h" +#include "remote_config/src/include/firebase/remote_config.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +void ExpectEqualConfigInfo(const ConfigInfo& l, const ConfigInfo& r) { + EXPECT_EQ(l.fetch_time, r.fetch_time); + EXPECT_EQ(l.last_fetch_status, r.last_fetch_status); + EXPECT_EQ(l.last_fetch_failure_reason, r.last_fetch_failure_reason); + EXPECT_EQ(l.throttled_end_time, r.throttled_end_time); +} + +TEST(RemoteConfigMetadataTest, Serialization) { + RemoteConfigMetadata remote_config_metadata; + remote_config_metadata.set_info( + ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + remote_config_metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + remote_config_metadata.AddSetting(kConfigSettingDeveloperMode, "0"); + + std::string buffer = remote_config_metadata.Serialize(); + RemoteConfigMetadata new_remote_config_metadata; + new_remote_config_metadata.Deserialize(buffer); + + EXPECT_EQ(remote_config_metadata, new_remote_config_metadata); +} + +TEST(RemoteConfigMetadataTest, GetInfoDefaultValues) { + RemoteConfigMetadata m; + ExpectEqualConfigInfo(m.info(), ConfigInfo({0, kLastFetchStatusSuccess, + kFetchFailureReasonInvalid, 0})); +} + +TEST(RemoteConfigMetadataTest, SetAndGetInfo) { + ConfigInfo info = {1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888}; + RemoteConfigMetadata m; + m.set_info(info); + ExpectEqualConfigInfo(m.info(), info); +} + +TEST(RemoteConfigMetadataTest, SetAndGetDigest) { + MetaDigestMap digest({{"namespace1", "digest1"}, {"namespace2", "digest2"}}); + + RemoteConfigMetadata m; + m.set_digest_by_namespace(digest); + + EXPECT_EQ(m.digest_by_namespace(), digest); +} + +TEST(RemoteConfigMetadataTest, SetAndGetSetting) { + RemoteConfigMetadata m; + EXPECT_EQ(m.GetSetting(kConfigSettingDeveloperMode), "0"); + + m.AddSetting(kConfigSettingDeveloperMode, "0"); + EXPECT_EQ(m.GetSetting(kConfigSettingDeveloperMode), "0"); + + m.AddSetting(kConfigSettingDeveloperMode, "1"); + EXPECT_EQ(m.GetSetting(kConfigSettingDeveloperMode), "1"); +} + +TEST(RemoteConfigMetadataTest, SetAndsettings) { + RemoteConfigMetadata m; + + std::map map; + EXPECT_EQ(m.settings(), map); + + m.AddSetting(kConfigSettingDeveloperMode, "0"); + map[kConfigSettingDeveloperMode] = "0"; + EXPECT_EQ(m.settings(), map); + + m.AddSetting(kConfigSettingDeveloperMode, "1"); + map[kConfigSettingDeveloperMode] = "1"; + EXPECT_EQ(m.settings(), map); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/notification_channel_test.cc b/remote_config/tests/desktop/notification_channel_test.cc new file mode 100644 index 0000000000..724215138d --- /dev/null +++ b/remote_config/tests/desktop/notification_channel_test.cc @@ -0,0 +1,79 @@ +// Copyright 2017 Google LLC +// +// 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 // NOLINT +#include // NOLINT +#include // NOLINT +#include "gtest/gtest.h" + +#include "remote_config/src/desktop/notification_channel.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +class NotificationChannelTest : public ::testing::Test { + protected: + int times_ = 0; + NotificationChannel channel_; +}; + +TEST_F(NotificationChannelTest, All) { + std::thread thread([this]() { + while (channel_.Get()) { + times_++; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } + }); + + EXPECT_EQ(times_, 0); + + // Thread will get `notification`. + channel_.Put(); + // Thread will get `notification` in short period of time + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // Expect thread got one notification. Thread is processing something now. + EXPECT_EQ(times_, 1); + + // Thread will get `notification` afrer current loop iteration. + channel_.Put(); + // Thread will get notification in short period of time + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // Expect thread got one `notification` total. It is processing something. + EXPECT_EQ(times_, 1); + + // Thread is doing something. It will get notification after finish first loop + // iteration. So channel will ignore this Put() call. + channel_.Put(); + // Thread will finish second loop iteration after sleep. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + // Expect thread got two `notification`s total. + EXPECT_EQ(times_, 2); + + // Thread will get notification that channel is closed. Thread will be closed. + channel_.Close(); + // Wait until thread will get `close notification`. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + // Thread should be closed, because channel is closed. + channel_.Put(); + // Still expect that thread got two `notification`s total. + EXPECT_EQ(times_, 2); + + thread.join(); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/remote_config_desktop_test.cc b/remote_config/tests/desktop/remote_config_desktop_test.cc new file mode 100644 index 0000000000..4902a663a6 --- /dev/null +++ b/remote_config/tests/desktop/remote_config_desktop_test.cc @@ -0,0 +1,528 @@ +// Copyright 2017 Google LLC +// +// 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 "remote_config/src/desktop/remote_config_desktop.h" + +#include // NOLINT + +#include "file/base/path.h" +#include "firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "firebase/future.h" +#include "remote_config/src/common.h" +#include "remote_config/src/desktop/config_data.h" +#include "remote_config/src/desktop/file_manager.h" +#include "remote_config/src/desktop/metadata.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +class RemoteConfigDesktopTest : public ::testing::Test { + protected: + void SetUp() override { + app_ = testing::CreateApp(); + + FutureData::Create(); + file_manager_ = new RemoteConfigFileManager( + file::JoinPath(FLAGS_test_tmpdir, "remote_config_data")); + SetUpInstance(); + } + + void TearDown() override { + delete instance_; + delete configs_; + delete file_manager_; + FutureData::Destroy(); + delete app_; + } + + // Remove previous instance and create the new one. New instance will load + // data from file, so we need to create file with data. + // + // After calling this function the `instance->configs_` must to be equal to + // the `configs_`. + void SetUpInstance() { + // !!! Remove previous instance at first, because Client can save data in + // background when you will rewriting the same file. + delete instance_; + SetupContent(); + EXPECT_TRUE(file_manager_->Save(*configs_)); + instance_ = new RemoteConfigInternal(*app_, *file_manager_); + } + + // Remove previous content and create the new one. + void SetupContent() { + uint64_t milliseconds_since_epoch = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + // Set this timestamp to guarantee passing fetching conditions. + NamespacedConfigData fetched( + NamespaceKeyValueMap( + {{"namespace2", {{"key1", "value1"}, {"key2", "value2"}}}}), + milliseconds_since_epoch - 2 * 1000 * kDefaultCacheExpiration); + NamespacedConfigData active( + NamespaceKeyValueMap({{RemoteConfigInternal::kDefaultNamespace, + {{"key_bool", "f"}, + {"key_long", "55555"}, + {"key_double", "100.5"}, + {"key_string", "aaa"}, + {"key_data", "zzz"}}}}), + 1234567); + NamespacedConfigData defaults( + NamespaceKeyValueMap({}), + 9999999); + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "1"); + + delete configs_; + configs_ = new LayeredConfigs(fetched, active, defaults, metadata); + } + + firebase::App* app_ = nullptr; + + RemoteConfigInternal* instance_ = nullptr; + LayeredConfigs* configs_ = nullptr; + RemoteConfigFileManager* file_manager_ = nullptr; +}; + +// Can't load `configs_` from file without permissions. +TEST_F(RemoteConfigDesktopTest, FailedLoadFromFile) { + RemoteConfigInternal instance( + *app_, RemoteConfigFileManager( + file::JoinPath(FLAGS_test_tmpdir, "not_found_file"))); + EXPECT_EQ(LayeredConfigs(), instance.configs_); +} + +TEST_F(RemoteConfigDesktopTest, SuccessLoadFromFile) { + EXPECT_EQ(*configs_, instance_->configs_); +} + +// Check async saving working well. +TEST_F(RemoteConfigDesktopTest, SuccessAsyncSaveToFile) { + // Let change the `configs_` variable. + instance_->configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap( + {{"new_namespace1", + {{"new_key1", "new_value1"}, {"new_key2", "new_value2"}}}}), + 999999); + + instance_->save_channel_.Put(); + + // Need to wait until background thread will save `configs_` to the file. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + LayeredConfigs new_content; + EXPECT_TRUE(file_manager_->Load(&new_content)); + EXPECT_EQ(new_content, instance_->configs_); +} + +TEST_F(RemoteConfigDesktopTest, SetDefaultsKeyValueVariant) { + { + SetUpInstance(); + + Variant vector_variant; + std::vector* std_vector_variant = + new std::vector(1, Variant::FromMutableBlob("123", 4)); + vector_variant.AssignVector(&std_vector_variant); + + ConfigKeyValueVariant defaults[] = { + ConfigKeyValueVariant{"key_bool", Variant(true)}, + ConfigKeyValueVariant{"key_blob", + Variant::FromMutableBlob("123456789", 9)}, + ConfigKeyValueVariant{"key_string", Variant("black")}, + ConfigKeyValueVariant{"key_long", Variant(120)}, + ConfigKeyValueVariant{"key_double", Variant(600.5)}, + // Will be ignored, this type is not supported. + ConfigKeyValueVariant{"key_vector_variant", vector_variant}}; + + instance_->SetDefaults(defaults, 6); + configs_->defaults.SetNamespace( + { + {"key_bool", "true"}, + {"key_blob", "123456789"}, + {"key_string", "black"}, + {"key_long", "120"}, + {"key_double", "600.5000000000000000"}, + }, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } + { + SetUpInstance(); + // `defaults` contains two keys `height`. The last one must to be applied. + ConfigKeyValueVariant defaults[] = { + ConfigKeyValueVariant{"height", Variant(100)}, + ConfigKeyValueVariant{"height", Variant(500)}, + ConfigKeyValueVariant{"width", Variant("120cm")}}; + instance_->SetDefaults(defaults, 3); + configs_->defaults.SetNamespace({{"height", "500"}, {"width", "120cm"}}, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } +} + +TEST_F(RemoteConfigDesktopTest, SetDefaultsKeyValue) { + { + SetUpInstance(); + ConfigKeyValue defaults[] = {ConfigKeyValue{"height", "100"}, + ConfigKeyValue{"height", "500"}, + ConfigKeyValue{"width", "120cm"}}; + instance_->SetDefaults(defaults, 3); + configs_->defaults.SetNamespace({{"height", "500"}, {"width", "120cm"}}, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } + { + SetUpInstance(); + ConfigKeyValue defaults[] = {ConfigKeyValue{"height", "100"}, + ConfigKeyValue{"height", "500"}, + ConfigKeyValue{"width", "120cm"}}; + instance_->SetDefaults(defaults, 3); + configs_->defaults.SetNamespace({{"height", "500"}, {"width", "120cm"}}, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } +} + +TEST_F(RemoteConfigDesktopTest, GetAndSetConfigSetting) { + EXPECT_EQ(instance_->GetConfigSetting(kConfigSettingDeveloperMode), "1"); + instance_->SetConfigSetting(kConfigSettingDeveloperMode, "0"); + EXPECT_EQ(instance_->GetConfigSetting(kConfigSettingDeveloperMode), "0"); +} + +TEST_F(RemoteConfigDesktopTest, GetBoolean) { + { EXPECT_FALSE(instance_->GetBoolean("key_bool", nullptr)); } + { + ValueInfo info; + EXPECT_FALSE(instance_->GetBoolean("key_bool", &info)); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetLong) { + { EXPECT_EQ(instance_->GetLong("key_long", nullptr), 55555); } + { + ValueInfo info; + EXPECT_EQ(instance_->GetLong("key_long", &info), 55555); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetDouble) { + { EXPECT_EQ(instance_->GetDouble("key_double", nullptr), 100.5); } + { + ValueInfo info; + EXPECT_EQ(instance_->GetDouble("key_double", &info), 100.5); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetString) { + { EXPECT_EQ(instance_->GetString("key_string", nullptr), "aaa"); } + { + ValueInfo info; + EXPECT_EQ(instance_->GetString("key_string", &info), "aaa"); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetData) { + { + EXPECT_THAT(instance_->GetData("key_data", nullptr), + ::testing::Eq(std::vector{'z', 'z', 'z'})); + } + { + ValueInfo info; + EXPECT_THAT(instance_->GetData("key_data", &info), + ::testing::Eq(std::vector{'z', 'z', 'z'})); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetKeys) { + { + EXPECT_THAT( + instance_->GetKeys(), + ::testing::Eq(std::vector{ + "key_bool", "key_data", "key_double", "key_long", "key_string"})); + } +} + +TEST_F(RemoteConfigDesktopTest, GetKeysByPrefix) { + { + EXPECT_THAT( + instance_->GetKeysByPrefix("key"), + ::testing::Eq(std::vector{ + "key_bool", "key_data", "key_double", "key_long", "key_string"})); + } + { + EXPECT_THAT( + instance_->GetKeysByPrefix("key_d"), + ::testing::Eq(std::vector{"key_data", "key_double"})); + } +} + +TEST_F(RemoteConfigDesktopTest, GetInfo) { + ConfigInfo info = instance_->GetInfo(); + EXPECT_EQ(info.fetch_time, 1498757224); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusPending); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonThrottled); + EXPECT_EQ(info.throttled_end_time, 1498758888); +} + +TEST_F(RemoteConfigDesktopTest, ActivateFetched) { + { + SetUpInstance(); + + instance_->configs_.fetched = NamespacedConfigData(); + instance_->configs_.active = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace:active", {{"key", "aaa"}}}}), 999999); + + // Will not activate, because the `fetched` configs is empty. + EXPECT_FALSE(instance_->ActivateFetched()); + } + { + SetUpInstance(); + + instance_->configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "aaa"}}}}), 999999); + instance_->configs_.active = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "aaa"}}}}), 999999); + + // Will not activate, because the `fetched` configs equal to the `active` + // configs, they have the same timestamp. + EXPECT_FALSE(instance_->ActivateFetched()); + } + { + SetUpInstance(); + instance_->configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace:fetched", {{"key1", "aaa"}}}}), + 9999999999); + instance_->configs_.active = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace:active", {{"key2", "zzz"}}}}), + 999999); + + // Will activate, because the `fetched` configs timestamp more than the + // `active` configs timestamp. + EXPECT_TRUE(instance_->ActivateFetched()); + EXPECT_EQ(instance_->configs_.fetched, instance_->configs_.active); + } +} + +TEST_F(RemoteConfigDesktopTest, Fetch) { + // Use fake rest implementation. In fake we just return some other metadata + // and fetched config and don't make HTTP requests. In this test case want + // make sure that all updated values apply correctly. + // + // See rest_fake.cc for more details. + { + SetUpInstance(); + instance_->Fetch(0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ(instance_->configs_.fetched, + NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "value"}}}}), + 1000000)); + + EXPECT_EQ(instance_->configs_.metadata.digest_by_namespace(), + MetaDigestMap({{"namespace", "digest"}})); + + ConfigInfo info = instance_->configs_.metadata.info(); + EXPECT_EQ(info.fetch_time, 0); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusSuccess); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonError); + EXPECT_EQ(info.throttled_end_time, 0); + + EXPECT_EQ( + instance_->configs_.metadata.GetSetting(kConfigSettingDeveloperMode), + "1"); + } + { + // Will fetch, because cache_expiration_in_seconds == 0. + SetUpInstance(); + Future future = instance_->Fetch(0); + EXPECT_EQ(future.status(), firebase::kFutureStatusPending); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(future.status(), firebase::kFutureStatusComplete); + } + { + // Will fetch, because cache is older than cache_expiration_in_seconds. + // We setup fetch.timestamp as + // milliseconds_since_epoch - 2*1000*cache_expiration_in_seconds; + SetUpInstance(); + Future future = instance_->Fetch(kDefaultCacheExpiration); + EXPECT_EQ(future.status(), firebase::kFutureStatusPending); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(future.status(), firebase::kFutureStatusComplete); + } + { + // Will NOT fetch, because cache is newer than kDefaultCacheExpiration + SetUpInstance(); + Future future = instance_->Fetch(10 * kDefaultCacheExpiration); + EXPECT_EQ(future.status(), firebase::kFutureStatusComplete); + } +} + +TEST_F(RemoteConfigDesktopTest, TestIsBoolTrue) { + // Confirm all the values that ARE BoolTrue. + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("1")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("true")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("t")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("on")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("yes")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("y")); + + // Ensure all the BoolFalse values are not BoolTrue. + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("0")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("false")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("f")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("no")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("n")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("off")); + + // Confirm a few random values. + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("apple")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("Yes")); // lower case only + EXPECT_FALSE( + RemoteConfigInternal::IsBoolTrue("100")); // only the number 1 exactly + EXPECT_FALSE( + RemoteConfigInternal::IsBoolTrue("-1")); // only the number 1 exactly + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("1.0")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("True")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("False")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("N")); // lower-case only +} + +TEST_F(RemoteConfigDesktopTest, TestIsBoolFalse) { + // Ensure all the BoolFalse values are not BoolTrue. + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("0")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("false")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("f")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("no")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("n")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("off")); + + // Confirm that the BoolTrue values are not BoolFalse. + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("1")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("true")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("t")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("on")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("yes")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("y")); + + // Confirm a few random values. + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("apple")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("Yes")); // lower case only + EXPECT_FALSE( + RemoteConfigInternal::IsBoolFalse("100")); // only the number 1 exactly + EXPECT_FALSE( + RemoteConfigInternal::IsBoolFalse("-1")); // only the number 1 exactly + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("1.0")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("True")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("False")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("N")); // lower-case only +} + +TEST_F(RemoteConfigDesktopTest, TestIsLong) { + EXPECT_TRUE(RemoteConfigInternal::IsLong("0")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("1")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("2")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("+0")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("+3")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("-5")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("8249")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("-718129")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("+9173923192819")); + + EXPECT_FALSE(RemoteConfigInternal::IsLong("0.0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong(" 5")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("9 ")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("- 8")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("-0-")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("-+0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("0-0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("1-1")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("12345+")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("12345-")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("12345abc")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("++81020")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("--32391")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("2+2=4")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("234,456")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("234.1")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("829.0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("1e100")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("")); + EXPECT_FALSE(RemoteConfigInternal::IsLong(" ")); +} + +TEST_F(RemoteConfigDesktopTest, TestIsDouble) { + EXPECT_TRUE(RemoteConfigInternal::IsDouble("0")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("2")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+0")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+3")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-5")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1.")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("8249")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-718129")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+9173923192819")); + + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1e10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1.2e9729")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("48.3e-39")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble(".4e+9")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-.289e11")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-7293e+72")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+489e322")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("10E10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("10E-10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-10E+10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+10E-10")); + + EXPECT_FALSE(RemoteConfigInternal::IsDouble("1.2e")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("1.9.2")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("1.3e8e2")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("-13-e8")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("98e4.3")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble(" 1")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("8 ")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("56.8f-29")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("-793e+89apple")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("489EEE")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("489EEE123")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble(" ")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("e")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble(".")); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/rest_test.cc b/remote_config/tests/desktop/rest_test.cc new file mode 100644 index 0000000000..4ec0776bdd --- /dev/null +++ b/remote_config/tests/desktop/rest_test.cc @@ -0,0 +1,502 @@ +// Copyright 2017 Google LLC +// +// 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 "remote_config/src/desktop/rest.h" + +#include +#include + +#include "app/rest/transport_builder.h" +#include "app/rest/transport_interface.h" +#include "app/rest/transport_mock.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "remote_config/src/desktop/rest_nanopb_encode.h" +#include "testing/config.h" +#include "net/proto2/public/text_format.h" +#include "zlib/zlibwrapper.h" +#include "wireless/android/config/proto/config.proto.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +class RemoteConfigRESTTest : public ::testing::Test { + protected: + void SetUp() override { + // Use TransportMock for testing instead of TransportCurl + rest::SetTransportBuilder([]() -> std::unique_ptr { + return std::unique_ptr(new rest::TransportMock); + }); + + firebase::AppOptions options = testing::MockAppOptions(); + options.set_package_name("com.google.samples.quickstart.config"); + options.set_app_id("1:290292664153:android:eddef00f8bd18e11"); + + app_ = testing::CreateApp(options); + + SetupContent(); + SetupProtoResponse(); + } + + void TearDown() override { delete app_; } + + void SetupContent() { + std::map empty_map; + NamespacedConfigData fetched( + NamespaceKeyValueMap({ + {"star_wars:droid", + {{"name", "BB-8"}, + {"height", "0.67 meters"}, + {"mass", "18 kilograms"}}}, + {"star_wars:starship", + {{"name", "Millennium Falcon"}, + {"length", "34.52–34.75 meters"}, + {"maximum_atmosphere_speed", "1,050 km/h"}}}, + {"star_wars:films", empty_map}, + {"star_wars:creatures", + {{"name", "Wampa"}, + {"height", "3 meters"}, + {"mass", "150 kilograms"}}}, + {"star_wars:locations", + {{"name", "Coruscant"}, + {"rotation_period", "24 standard hours"}, + {"orbital_period", "365 standard days"}}}, + }), + MillisecondsSinceEpoch() - 7 * 3600 * 1000); // 7 hours ago. + NamespacedConfigData active( + NamespaceKeyValueMap({{"star_wars:droid", + {{"name", "R2-D2"}, + {"height", "1.09 meters"}, + {"mass", "32 kilograms"}}}, + {"star_wars:starship", + {{"name", "Imperial I-class Star Destroyer"}, + {"length", "1,600 meters"}, + {"maximum_atmosphere_speed", "975 km/h"}}}}), + MillisecondsSinceEpoch() - 10 * 3600 * 1000); // 10 hours ago. + // Can be empty for testing. + NamespacedConfigData defaults(NamespaceKeyValueMap(), 0); + + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo( + {MillisecondsSinceEpoch() - 7 * 3600 * 1000 /* 7 hours ago */, + kLastFetchStatusSuccess, kFetchFailureReasonInvalid, 0})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"star_wars:droid", "DROID_DIGEST"}, + {"star_wars:starship", "STARSHIP_DIGEST"}, + {"star_wars:films", "FILMS_DIGEST"}, + {"star_wars:creatures", "CREATURES_DIGEST"}, + {"star_wars:locations", "LOCATIONS_DIGEST"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "1"); + + configs_ = LayeredConfigs(fetched, active, defaults, metadata); + } + + void SetupProtoResponse() { + std::string text = + "app_config {" + " app_name: \"com.google.samples.quickstart.config\"" + + // UPDATE, add new namespace. + " namespace_config {" + " namespace: \"star_wars:vehicle\"" + " digest: \"VEHICLE_NEW_DIGEST\"" + " status: UPDATE" + " entry {key: \"name\" value: \"All Terrain Armored Transport\"}" + " entry {key: \"passengers\" value: \"40 troops\"}" + " entry {key: \"cargo_capacity\" value: \"3,500 metric tons\"}" + " }" + + // UPDATE, update existed namespace. + " namespace_config {" + " namespace: \"star_wars:starship\"" + " digest: \"STARSHIP_NEW_DIGEST\"" + " status: UPDATE" + " entry {key: \"name\" value: \"Imperial I-class Star Destroyer\"}" + " entry {key: \"length\" value: \"1,600 meters\"}" + " entry {key: \"maximum_atmosphere_speed\" value: \"975 km/h\"}" + " }" + + // NO_TEMPLATE for existed namespace. Remove digest and namespace. + " namespace_config {" + " namespace: \"star_wars:films\" status: NO_TEMPLATE" + " }" + + // NO_TEMPLATE for NOT existed namespace. Will be ignored. + " namespace_config {" + " namespace: \"star_wars:spinoff_films\" status: NO_TEMPLATE" + " }" + + // NO_CHANGE for existed namespace. Only digest will be updated. + " namespace_config {" + " namespace: \"star_wars:droid\"" + " digest: \"DROID_NEW_DIGEST\"" + " status: NO_CHANGE" + " }" + + // EMPTY_CONFIG for existed namespace. Clear namespace and update + // digest. + " namespace_config {" + " namespace: \"star_wars:creatures\"" + " digest: \"CREATURES_NEW_DIGEST\"" + " status: EMPTY_CONFIG" + " }" + + // EMPTY_CONFIG for NOT existed namespace. Create empty namespace and + // add new digest to map. + " namespace_config {" + " namespace: \"star_wars:duels\"" + " digest: \"DUELS_NEW_DIGEST\"" + " status: EMPTY_CONFIG" + " }" + + // NOT_AUTHORIZED for existed namespace. Remove namespace and digest. + " namespace_config {" + " namespace: \"star_wars:locations\"" + " status: NOT_AUTHORIZED" + " }" + + // NOT_AUTHORIZED for NOT existed namespace. Will be ignored. + " namespace_config {" + " namespace: \"star_wars:video_games\"" + " status: NOT_AUTHORIZED" + " }" + + "}"; + + EXPECT_TRUE(proto2::TextFormat::ParseFromString(text, &proto_response_)); + } + + // This was moved from the code that used to build proto requests when + // protosbufs were used directly. It can live here because the tests can + // still depend on protobufs and gives us a way to validate nanopbs are + // encoded the same way as the original protos. + android::config::ConfigFetchRequest GetProtoFetchRequestData( + const RemoteConfigREST& rest) { + android::config::ConfigFetchRequest proto_request; + proto_request.set_client_version(2); + proto_request.set_device_type(5); + proto_request.set_device_subtype(10); + + android::config::PackageData* package_data = + proto_request.add_package_data(); + package_data->set_package_name(rest.app_package_name_); + package_data->set_gmp_project_id(rest.app_gmp_project_id_); + + for (const auto& keyvalue : rest.configs_.metadata.digest_by_namespace()) { + android::config::NamedValue* named_value = + package_data->add_namespace_digest(); + named_value->set_name(keyvalue.first); + named_value->set_value(keyvalue.second); + } + + // Check if developer mode enable + if (rest.configs_.metadata.GetSetting(kConfigSettingDeveloperMode) == "1") { + android::config::NamedValue* named_value = + package_data->add_custom_variable(); + named_value->set_name(kDeveloperModeKey); + named_value->set_value("1"); + } + + // Need iid for next two fields + // package_data->set_app_instance_id("fake instance id"); + // package_data->set_app_instance_id_token("fake instance id token"); + + package_data->set_requested_cache_expiration_seconds( + static_cast(rest.cache_expiration_in_seconds_)); + + if (rest.configs_.fetched.timestamp() == 0) { + package_data->set_fetched_config_age_seconds(-1); + } else { + package_data->set_fetched_config_age_seconds(static_cast( + (MillisecondsSinceEpoch() - rest.configs_.fetched.timestamp()) / + 1000)); + } + + package_data->set_sdk_version(SDK_MAJOR_VERSION * 10000 + + SDK_MINOR_VERSION * 100 + SDK_PATCH_VERSION); + + if (rest.configs_.active.timestamp() == 0) { + package_data->set_active_config_age_seconds(-1); + } else { + package_data->set_active_config_age_seconds(static_cast( + (MillisecondsSinceEpoch() - rest.configs_.active.timestamp()) / + 1000)); + } + return proto_request; + } + + // Check all values in case when fetch failed. + void ExpectFetchFailure(const RemoteConfigREST& rest, int code) { + EXPECT_EQ(rest.rest_response_.status(), code); + EXPECT_TRUE(rest.rest_response_.header_completed()); + EXPECT_TRUE(rest.rest_response_.body_completed()); + + EXPECT_EQ(rest.fetched().config(), configs_.fetched.config()); + EXPECT_EQ(rest.metadata().digest_by_namespace(), + configs_.metadata.digest_by_namespace()); + + ConfigInfo info = rest.metadata().info(); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusFailure); + EXPECT_LE(info.fetch_time, MillisecondsSinceEpoch()); + EXPECT_GE(info.fetch_time, MillisecondsSinceEpoch() - 10000); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonError); + EXPECT_LE(info.throttled_end_time, MillisecondsSinceEpoch()); + EXPECT_GE(info.throttled_end_time, MillisecondsSinceEpoch() - 10000); + } + + uint64_t MillisecondsSinceEpoch() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } + + std::string GzipCompress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_size = ZLib::MinCompressbufSize(input.length()); + std::unique_ptr result(new char[result_size]); + int err = zlib.Compress( + reinterpret_cast(result.get()), &result_size, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_size); + } + + std::string GzipDecompress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_length = zlib.GzipUncompressedLength( + reinterpret_cast(input.data()), input.length()); + std::unique_ptr result(new char[result_length]); + int err = zlib.Uncompress( + reinterpret_cast(result.get()), &result_length, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_length); + } + + firebase::App* app_ = nullptr; + + LayeredConfigs configs_; + + rest::Response rest_response_; + android::config::ConfigFetchResponse proto_response_; +}; + +// Check correctness protobuf object setup for REST request. +TEST_F(RemoteConfigRESTTest, SetupProto) { + RemoteConfigREST rest(app_->options(), configs_, 3600); + ConfigFetchRequest request_data = rest.GetFetchRequestData(); + + EXPECT_EQ(request_data.client_version, 2); + // Not handling repeated package_data since spec says there's only 1. + + PackageData& package_data = request_data.package_data; + EXPECT_EQ(package_data.package_name, app_->options().package_name()); + EXPECT_EQ(package_data.gmp_project_id, app_->options().app_id()); + + // Check digests + std::map digests; + for (const auto& item : package_data.namespace_digest) { + digests[item.first] = item.second; + } + EXPECT_THAT(digests, ::testing::Eq(std::map( + {{"star_wars:droid", "DROID_DIGEST"}, + {"star_wars:starship", "STARSHIP_DIGEST"}, + {"star_wars:films", "FILMS_DIGEST"}, + {"star_wars:creatures", "CREATURES_DIGEST"}, + {"star_wars:locations", "LOCATIONS_DIGEST"}}))); + + // Check developers settings + std::map settings; + for (const auto& item : package_data.custom_variable) { + settings[item.first] = item.second; + } + EXPECT_THAT(settings, ::testing::Eq(std::map( + {{"_rcn_developer", "1"}}))); + + // The same value as in RemoteConfigRest constructor. + EXPECT_EQ(package_data.requested_cache_expiration_seconds, 3600); + + // Fetched age should be in range [7hours, 7hours + eps], + // where eps - some small value in seconds. + EXPECT_GE(package_data.fetched_config_age_seconds, 7 * 3600); + EXPECT_LE(package_data.fetched_config_age_seconds, 7 * 3600 + 10); + + // Active age should be in range [10hours, 10hours + eps], + // where eps - some small value in seconds. + EXPECT_GE(package_data.active_config_age_seconds, 10 * 3600); + EXPECT_LE(package_data.active_config_age_seconds, 10 * 3600 + 10); +} + +// Check correctness REST request setup. +TEST_F(RemoteConfigRESTTest, SetupRESTRequest) { + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.SetupRestRequest(); + + firebase::rest::RequestOptions request_options = rest.rest_request_.options(); + EXPECT_EQ(request_options.url, kServerURL); + EXPECT_EQ(request_options.method, kHTTPMethodPost); + std::string post_fields; + EXPECT_TRUE(rest.rest_request_.ReadBodyIntoString(&post_fields)); + + ConfigFetchRequest fetch_data = rest.GetFetchRequestData(); + std::string encoded_str = EncodeFetchRequest(fetch_data); + + EXPECT_EQ(GzipDecompress(post_fields), encoded_str); + EXPECT_NE(request_options.header.find("Content-Type"), + request_options.header.end()); + EXPECT_EQ(request_options.header["Content-Type"], + "application/x-protobuffer"); + EXPECT_NE(request_options.header.find("x-goog-api-client"), + request_options.header.end()); + EXPECT_THAT(request_options.header["x-goog-api-client"], + ::testing::HasSubstr("fire-cpp/")); + + // Setup a proto directly with the request data. + android::config::ConfigFetchRequest proto_data = + GetProtoFetchRequestData(rest); + std::string proto_str = proto_data.SerializeAsString(); + EXPECT_EQ(proto_str, encoded_str); + // If a proto encode doesn't match, the strings aren't easily printable, so + // the following makes it easier to examine the discrepancies. + if (encoded_str != proto_str) { + printf("--------- Encoded Proto ------------\n"); + android::config::ConfigFetchRequest proto_parse; + proto_parse.ParseFromString(encoded_str); + printf("%s\n", proto_parse.DebugString().c_str()); + printf("-------- Reference Proto -----------\n"); + printf("%s\n", proto_data.DebugString().c_str()); + printf("------------------------------------\n"); + + int max_len = (encoded_str.length() > proto_str.length()) + ? encoded_str.length() + : proto_str.length(); + printf("encoded size: %d reference size: %d\n", + static_cast(encoded_str.length()), + static_cast(proto_str.length())); + for (int i = 0; i < max_len; i++) { + char oldc = (i < proto_str.length()) ? proto_str.c_str()[i] : 0; + char newc = (i < encoded_str.length()) ? encoded_str.c_str()[i] : 0; + printf("%02X (%03d) '%c' %02X (%03d) '%c'\n", + newc, newc, newc, + oldc, oldc, oldc); + } + } +} + +// Can't pass binary body response to testing::cppsdk::ConfigSet. Can configure +// only response with not gzip body. +// +// Test passing http request to mock transport and get http +// response with error or with empty body. +// +// We have 2 different cases: +// +// 1) response code is 200. Response body is empty, because can't gunzip not +// gzip body. +// +// 2) response code is 400. Will not try gunzip body, but it's still failure, +// because response code is not 200. +TEST_F(RemoteConfigRESTTest, Fetch) { + int codes[] = {200, 400}; + for (int code : codes) { + char config[1000]; + snprintf(config, sizeof(config), + "{" + " config:[" + " {fake:'%s'," + " httpresponse: {" + " header: ['HTTP/1.1 %d Ok','Server:mock server 101']," + " body: ['some body, not proto, not gzip',]" + " }" + " }" + " ]" + "}", + kServerURL, code); + firebase::testing::cppsdk::ConfigSet(config); + + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.Fetch(*app_); + + ExpectFetchFailure(rest, code); + } +} + +TEST_F(RemoteConfigRESTTest, ParseRestResponseProtoFailure) { + std::string header = "HTTP/1.1 200 Ok"; + std::string body = GzipCompress("some fake body, NOT proto"); + + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.rest_response_.ProcessHeader(header.data(), header.length()); + rest.rest_response_.ProcessBody(body.data(), body.length()); + rest.rest_response_.MarkCompleted(); + EXPECT_EQ(rest.rest_response_.status(), 200); + + rest.ParseRestResponse(); + + ExpectFetchFailure(rest, 200); +} + +TEST_F(RemoteConfigRESTTest, ParseRestResponseSuccess) { + std::string header = "HTTP/1.1 200 Ok"; + std::string body = GzipCompress(proto_response_.SerializeAsString()); + + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.rest_response_.ProcessHeader(header.data(), header.length()); + rest.rest_response_.ProcessBody(body.data(), body.length()); + rest.rest_response_.MarkCompleted(); + EXPECT_EQ(rest.rest_response_.status(), 200); + + rest.ParseRestResponse(); + + std::map empty_map; + EXPECT_THAT(rest.fetched().config(), + ::testing::ContainerEq(NamespaceKeyValueMap({ + {"star_wars:vehicle", + {{"name", "All Terrain Armored Transport"}, + {"passengers", "40 troops"}, + {"cargo_capacity", "3,500 metric tons"}}}, + {"star_wars:droid", + {{"name", "BB-8"}, + {"height", "0.67 meters"}, + {"mass", "18 kilograms"}}}, + {"star_wars:starship", + {{"name", "Imperial I-class Star Destroyer"}, + {"length", "1,600 meters"}, + {"maximum_atmosphere_speed", "975 km/h"}}}, + {"star_wars:creatures", empty_map}, + {"star_wars:duels", empty_map}, + }))); + + EXPECT_THAT(rest.metadata().digest_by_namespace(), + ::testing::ContainerEq(MetaDigestMap( + {{"star_wars:vehicle", "VEHICLE_NEW_DIGEST"}, + {"star_wars:starship", "STARSHIP_NEW_DIGEST"}, + {"star_wars:droid", "DROID_NEW_DIGEST"}, + {"star_wars:creatures", "CREATURES_NEW_DIGEST"}, + {"star_wars:duels", "DUELS_NEW_DIGEST"}}))); + + ConfigInfo info = rest.metadata().info(); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusSuccess); + EXPECT_LE(info.fetch_time, MillisecondsSinceEpoch()); + EXPECT_GE(info.fetch_time, MillisecondsSinceEpoch() - 10000); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/remote_config_test.cc b/remote_config/tests/remote_config_test.cc new file mode 100644 index 0000000000..b18888ee83 --- /dev/null +++ b/remote_config/tests/remote_config_test.cc @@ -0,0 +1,692 @@ +// Copyright 2017 Google LLC +// +// 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. + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "remote_config/src/include/firebase/remote_config.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include +#include + +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" +#include "firebase/variant.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace remote_config { + +class RemoteConfigTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + reporter_.reset(); + InitializeRemoteConfig(); + } + + void TearDown() override { + Terminate(); + delete firebase_app_; + firebase_app_ = nullptr; + + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + void InitializeRemoteConfig() { + firebase_app_ = testing::CreateApp(); + EXPECT_NE(firebase_app_, nullptr) << "Init app failed"; + + InitResult result = Initialize(*firebase_app_); + EXPECT_NE(firebase_app_, nullptr) << "Init app failed"; + EXPECT_EQ(result, kInitResultSuccess); + } + + App* firebase_app_ = nullptr; + firebase::testing::cppsdk::Reporter reporter_; +}; + +#define REPORT_EXPECT(fake, result, ...) \ + reporter_.addExpectation(fake, result, firebase::testing::cppsdk::kAny, \ + __VA_ARGS__) + +#define REPORT_EXPECT_PLATFORM(fake, result, platform, ...) \ + reporter_.addExpectation(fake, result, platform, __VA_ARGS__) + +// Check SetUp and TearDown working well. +TEST_F(RemoteConfigTest, InitializeAndTerminate) {} + +TEST_F(RemoteConfigTest, InitializeTwice) { + InitResult result = Initialize(*firebase_app_); + EXPECT_EQ(result, kInitResultSuccess); +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +TEST_F(RemoteConfigTest, SetDefaultsOnAndroid) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", {"0"}); + SetDefaults(0); +} + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +TEST_F(RemoteConfigTest, SetDefaultsWithNullConfigKeyValueVariant) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", {"{}"}); + ConfigKeyValueVariant* keyvalues = nullptr; + SetDefaults(keyvalues, 0); +} + +TEST_F(RemoteConfigTest, SetDefaultsWithConfigKeyValueVariant) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", + {"{color=black, height=120}"}); + + ConfigKeyValueVariant defaults[] = { + ConfigKeyValueVariant{"color", Variant("black")}, + ConfigKeyValueVariant{"height", Variant(120)}}; + + SetDefaults(defaults, 2); +} + +TEST_F(RemoteConfigTest, SetDefaultsWithNullConfigKeyValue) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", {"{}"}); + ConfigKeyValue* keyvalues = nullptr; + SetDefaults(keyvalues, 0); +} + +TEST_F(RemoteConfigTest, SetDefaultsWithConfigKeyValue) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", + {"{color=black, height=120, width=600.5}"}); + + ConfigKeyValue defaults[] = {ConfigKeyValue{"color", "black"}, + ConfigKeyValue{"height", "120"}, + ConfigKeyValue{"width", "600.5"}}; + + SetDefaults(defaults, 3); +} + +TEST_F(RemoteConfigTest, GetConfigSettingTrue) { + REPORT_EXPECT("FirebaseRemoteConfig.getInfo", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.isDeveloperModeEnabled", "true", + {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigSettings.isDeveloperModeEnabled'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + EXPECT_EQ(GetConfigSetting(kConfigSettingDeveloperMode), "1"); +} + +TEST_F(RemoteConfigTest, GetConfigSettingFalse) { + REPORT_EXPECT("FirebaseRemoteConfig.getInfo", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.isDeveloperModeEnabled", "false", + {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigSettings.isDeveloperModeEnabled'," + " returnvalue: {'tbool': false}}" + " ]" + "}"); + EXPECT_EQ(GetConfigSetting(kConfigSettingDeveloperMode), "0"); +} + +TEST_F(RemoteConfigTest, SetConfigSettingTrue) { + REPORT_EXPECT("FirebaseRemoteConfig.setConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled", + "", {"true"}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.build", "", {}); + SetConfigSetting(kConfigSettingDeveloperMode, "1"); +} + +TEST_F(RemoteConfigTest, SetConfigSettingFalse) { + REPORT_EXPECT("FirebaseRemoteConfig.setConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled", + "", {"false"}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.build", "", {}); + SetConfigSetting(kConfigSettingDeveloperMode, "0"); +} + +// Start check GetBoolean functions +TEST_F(RemoteConfigTest, GetBooleanNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getBoolean", "false", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getBoolean'," + " returnvalue: {'tbool': false}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_FALSE(GetBoolean(key)); +} + +TEST_F(RemoteConfigTest, GetBooleanKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getBoolean", "true", {"give_prize"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getBoolean'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + EXPECT_TRUE(GetBoolean("give_prize")); +} + +TEST_F(RemoteConfigTest, GetBooleanKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"give_prize"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asBoolean", "true", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asBoolean'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_TRUE(GetBoolean("give_prize", info)); +} + +TEST_F(RemoteConfigTest, GetBooleanKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"give_prize"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asBoolean", "true", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asBoolean'," + " returnvalue: {'tbool': true}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_TRUE(GetBoolean("give_prize", &info)); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetBoolean functions + +// Start check GetLong functions +TEST_F(RemoteConfigTest, GetLongNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getLong", "1000", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getLong'," + " returnvalue: {'tlong': 1000}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_EQ(GetLong(key), 1000); +} + +TEST_F(RemoteConfigTest, GetLongKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getLong", "1000000000", {"price"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getLong'," + " returnvalue: {'tlong': 1000000000}}" + " ]" + "}"); + EXPECT_EQ(GetLong("price"), 1000000000); +} + +TEST_F(RemoteConfigTest, GetLongKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asLong", "100", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asLong'," + " returnvalue: {'tlong': 100}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_EQ(GetLong("wallet_cash", info), 100); +} + +TEST_F(RemoteConfigTest, GetLongKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asLong", "7000000", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asLong'," + " returnvalue: {'tlong': 7000000}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_EQ(GetLong("wallet_cash", &info), 7000000); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetLong functions + +// Start check GetDouble functions +TEST_F(RemoteConfigTest, GetDoubleNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getDouble", "1000.500", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getDouble'," + " returnvalue: {'tdouble': 1000.500}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_EQ(GetDouble(key), 1000.500); +} + +TEST_F(RemoteConfigTest, GetDoubleKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getDouble", "1000000000.000", {"price"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getDouble'," + " returnvalue: {'tdouble': 1000000000.000}}" + " ]" + "}"); + EXPECT_EQ(GetDouble("price"), 1000000000.000); +} + +TEST_F(RemoteConfigTest, GetDoubleKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asDouble", "100.999", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asDouble'," + " returnvalue: {'tdouble': 100.999}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_EQ(GetDouble("wallet_cash", info), 100.999); +} + +TEST_F(RemoteConfigTest, GetDoubleKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asDouble", "7000000.000", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asDouble'," + " returnvalue: {'tdouble': 7000000.000}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_EQ(GetDouble("wallet_cash", &info), 7000000.000); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetDouble functions + +// Start check GetString functions +TEST_F(RemoteConfigTest, GetStringNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getString", "I am fake", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getString'," + " returnvalue: {'tstring': 'I am fake'}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_EQ(GetString(key), "I am fake"); +} + +TEST_F(RemoteConfigTest, GetStringKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getString", "I am fake", {"price"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getString'," + " returnvalue: {'tstring': 'I am fake'}}" + " ]" + "}"); + EXPECT_EQ(GetString("price"), "I am fake"); +} + +TEST_F(RemoteConfigTest, GetStringKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asString", "I am fake", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asString'," + " returnvalue: {'tstring': 'I am fake'}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_EQ(GetString("wallet_cash", info), "I am fake"); +} + +TEST_F(RemoteConfigTest, GetStringKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asString", "I am fake", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asString'," + " returnvalue: {'tstring': 'I am fake'}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_EQ(GetString("wallet_cash", &info), "I am fake"); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetString functions + +// Start check GetData functions +TEST_F(RemoteConfigTest, GetDataNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getByteArray", "abcd", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getByteArray'," + " returnvalue: {'tstring': 'abcd'}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_THAT(GetData(key), + ::testing::Eq(std::vector({'a', 'b', 'c', 'd'}))); +} + +TEST_F(RemoteConfigTest, GetDataKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getByteArray", "abc", {"name"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getByteArray'," + " returnvalue: {'tstring': 'abc'}}" + " ]" + "}"); + EXPECT_THAT(GetData("name"), + ::testing::Eq(std::vector({'a', 'b', 'c'}))); +} + +TEST_F(RemoteConfigTest, GetDataKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asByteArray", "xyz", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asByteArray'," + " returnvalue: {'tstring': 'xyz'}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_THAT(GetData("wallet_cash", info), + ::testing::Eq(std::vector({'x', 'y', 'z'}))); +} + +TEST_F(RemoteConfigTest, GetDataKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asByteArray", "xyz", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asByteArray'," + " returnvalue: {'tstring': 'xyz'}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_THAT(GetData("wallet_cash", &info), + ::testing::Eq(std::vector({'x', 'y', 'z'}))); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetData functions + +// Start check GetKeysByPrefix functions +TEST_F(RemoteConfigTest, GetKeysByPrefix) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[1, 2, 3, 4]", + {"some_prefix"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[1, 2, 3, 4]'}}" + " ]" + "}"); + EXPECT_THAT(GetKeysByPrefix("some_prefix"), + ::testing::Eq(std::vector({"1", "2", "3", "4"}))); +} + +TEST_F(RemoteConfigTest, GetKeysByPrefixEmptyResult) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[]", {"some_prefix"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[]'}}" + " ]" + "}"); + EXPECT_THAT(GetKeysByPrefix("some_prefix"), + ::testing::Eq(std::vector({}))); +} + +TEST_F(RemoteConfigTest, GetKeysByPrefixNullPrefix) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[1, 2, 3, 4]", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[1, 2, 3, 4]'}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_THAT(GetKeysByPrefix(key), + ::testing::Eq(std::vector({"1", "2", "3", "4"}))); +} +// Finish check GetKeysByPrefix functions + +// Start check GetKeys functions +TEST_F(RemoteConfigTest, GetKeys) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[1, 2, 3, 4]", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[1, 2, 3, 4]'}}" + " ]" + "}"); + EXPECT_THAT(GetKeys(), + ::testing::Eq(std::vector({"1", "2", "3", "4"}))); +} +// Finish check GetKeys functions + +void Verify(const Future& result) { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); +} + +TEST_F(RemoteConfigTest, Fetch) { + // Default value: 43200seconds = 12hours + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"43200"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Verify(Fetch()); +} + +TEST_F(RemoteConfigTest, FetchWithException) { + // Default value: 43200seconds = 12hours + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"43200"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{throwexception:true," + " exceptionmsg:'fetch failed'," + " ticker:1}}" + " ]" + "}"); + Verify(Fetch()); +} + +TEST_F(RemoteConfigTest, FetchWithExpiration) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Verify(Fetch(3600)); +} + +TEST_F(RemoteConfigTest, FetchWithExpirationAndException) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{throwexception:true," + " exceptionmsg:'fetch failed'," + " ticker:1}}" + " ]" + "}"); + Verify(Fetch(3600)); +} + +TEST_F(RemoteConfigTest, FetchLastResult) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Future result = Fetch(3600); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + EXPECT_EQ(firebase::kFutureStatusPending, FetchLastResult().status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(firebase::kFutureStatusComplete, FetchLastResult().status()); +} + +TEST_F(RemoteConfigTest, FetchLastResultWithCallFetchTwice) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Future result1 = Fetch(3600); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result1.status()); + EXPECT_EQ(firebase::kFutureStatusPending, FetchLastResult().status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result1.status()); + EXPECT_EQ(firebase::kFutureStatusComplete, FetchLastResult().status()); + + firebase::testing::cppsdk::TickerReset(); + + Future result2 = Fetch(3600); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result2.status()); + EXPECT_EQ(firebase::kFutureStatusPending, FetchLastResult().status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result2.status()); + EXPECT_EQ(firebase::kFutureStatusComplete, FetchLastResult().status()); +} + +TEST_F(RemoteConfigTest, ActivateFetchedTrue) { + REPORT_EXPECT("FirebaseRemoteConfig.activateFetched", "true", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.activateFetched'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + EXPECT_TRUE(ActivateFetched()); +} + +TEST_F(RemoteConfigTest, ActivateFetchedFalse) { + REPORT_EXPECT("FirebaseRemoteConfig.activateFetched", "false", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.activateFetched'," + " returnvalue: {'tbool': false}}" + " ]" + "}"); + EXPECT_FALSE(ActivateFetched()); +} + +TEST_F(RemoteConfigTest, GetInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getInfo", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getFetchTimeMillis", "1000", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getLastFetchStatus", "2", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigInfo.getFetchTimeMillis'," + " returnvalue: {'tlong': 1000}}," + " {fake:'FirebaseRemoteConfigInfo.getLastFetchStatus'," + " returnvalue: {'tint': 2}}," + " ]" + "}"); + const ConfigInfo info = GetInfo(); + EXPECT_EQ(info.fetch_time, 1000); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusFailure); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonThrottled); +} +} // namespace remote_config +} // namespace firebase diff --git a/storage/src/common/storage_uri_parser_test.cc b/storage/src/common/storage_uri_parser_test.cc new file mode 100644 index 0000000000..53695bea6f --- /dev/null +++ b/storage/src/common/storage_uri_parser_test.cc @@ -0,0 +1,153 @@ +// Copyright 2018 Google LLC +// +// 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 "storage/src/common/storage_uri_parser.h" + +#include + +#include "app/src/include/firebase/internal/common.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace storage { +namespace internal { + +struct UriAndComponents { + // URI to parse. + const char* path; + // Expected bucket from URI. + const char* expected_bucket; + // Expected path from URI. + const char* expected_path; +}; + +TEST(StorageUriParserTest, TestInvalidUris) { + EXPECT_FALSE(UriToComponents("", "test", nullptr, nullptr)); + EXPECT_FALSE(UriToComponents("invalid://uri", "test", nullptr, nullptr)); +} + +TEST(StorageUriParserTest, TestValidUris) { + EXPECT_TRUE( + UriToComponents("gs://somebucket", "gs_scheme", nullptr, nullptr)); + EXPECT_TRUE(UriToComponents("http://domain/b/bucket", "http_scheme", nullptr, + nullptr)); + EXPECT_TRUE(UriToComponents("https://domain/b/bucket", // NOTYPO + "http_scheme", nullptr, nullptr)); +} + +// Extract components from each URI in uri_and_expected_components and compare +// with the expectedBucket & expectedPath values in the specified structure. +// object_prefix is used as a prefix for the object name supplied to each +// call to UriToComponents() to aid debugging when an error is reported by the +// method. +static void ExtractComponents( + const UriAndComponents* uri_and_expected_components, + size_t number_of_uri_and_expected_components, + const std::string& object_prefix) { + for (size_t i = 0; i < number_of_uri_and_expected_components; ++i) { + const auto& param = uri_and_expected_components[i]; + { + std::string bucket; + EXPECT_TRUE(UriToComponents( + param.path, (object_prefix + "_bucket").c_str(), &bucket, nullptr)); + EXPECT_EQ(param.expected_bucket, bucket); + } + { + std::string path; + EXPECT_TRUE(UriToComponents(param.path, (object_prefix + "_path").c_str(), + nullptr, &path)); + EXPECT_EQ(param.expected_path, path); + } + { + std::string bucket; + std::string path; + EXPECT_TRUE(UriToComponents(param.path, (object_prefix + "_all").c_str(), + &bucket, &path)); + EXPECT_EQ(param.expected_bucket, bucket); + EXPECT_EQ(param.expected_path, path); + } + } +} + +TEST(StorageUriParserTest, TestExtractGsSchemeComponents) { + const UriAndComponents kTestParams[] = { + { + "gs://somebucket", + "somebucket", + "", + }, + { + "gs://somebucket/", + "somebucket", + "", + }, + { + "gs://somebucket/a/path/to/an/object", + "somebucket", + "/a/path/to/an/object", + }, + { + "gs://somebucket/a/path/to/an/object/", + "somebucket", + "/a/path/to/an/object", + }, + }; + ExtractComponents(kTestParams, FIREBASE_ARRAYSIZE(kTestParams), "gsscheme"); +} + +TEST(StorageUriParserTest, TestExtractHttpHttpsSchemeComponents) { + const UriAndComponents kTestParams[] = { + { + "http://firebasestorage.googleapis.com/v0/b/somebucket", + "somebucket", + "", + }, + { + "http://firebasestorage.googleapis.com/v0/b/somebucket/", + "somebucket", + "", + }, + { + "http://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object", + "somebucket", + "/an/object", + }, + { + "http://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object/", + "somebucket", + "/an/object", + }, + { + "https://firebasestorage.googleapis.com/v0/b/somebucket/", + "somebucket", + "", + }, + { + "https://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object", + "somebucket", + "/an/object", + }, + { + "https://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object/", + "somebucket", + "/an/object", + }, + }; + ExtractComponents(kTestParams, FIREBASE_ARRAYSIZE(kTestParams), "http(s)"); +} + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/tests/CMakeLists.txt b/storage/tests/CMakeLists.txt new file mode 100644 index 0000000000..d4ec18d6a0 --- /dev/null +++ b/storage/tests/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright 2019 Google LLC +# +# 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. + +firebase_cpp_cc_test( + firebase_storage_desktop_utils_test + SOURCES + desktop/storage_desktop_utils_tests.cc + DEPENDS + firebase_app_for_testing + firebase_rest_lib + firebase_storage + firebase_testing +) + diff --git a/storage/tests/desktop/storage_desktop_utils_tests.cc b/storage/tests/desktop/storage_desktop_utils_tests.cc new file mode 100644 index 0000000000..9726e6655e --- /dev/null +++ b/storage/tests/desktop/storage_desktop_utils_tests.cc @@ -0,0 +1,195 @@ +// Copyright 2017 Google LLC +// +// 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 + +#include "app/rest/util.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "storage/src/desktop/controller_desktop.h" +#include "storage/src/desktop/metadata_desktop.h" +#include "storage/src/desktop/storage_path.h" +#include "storage/src/desktop/storage_reference_desktop.h" +#include "testing/json_util.h" + +namespace { + +using firebase::App; +using firebase::storage::internal::MetadataInternal; +using firebase::storage::internal::StorageInternal; +using firebase::storage::internal::StoragePath; +using firebase::storage::internal::StorageReferenceInternal; + +// The fixture for testing helper classes for storage desktop. +class StorageDesktopUtilsTests : public ::testing::Test { + protected: + void SetUp() override { firebase::rest::util::Initialize(); } + + void TearDown() override { firebase::rest::util::Terminate(); } +}; + +// Test the GS URI-based StoragePath constructors +TEST_F(StorageDesktopUtilsTests, testGSStoragePathConstructors) { + StoragePath test_path; + + // Test basic case: + test_path = StoragePath("gs://Bucket/path/Object"); + + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/Object"); + + // Test a more complex path: + test_path = StoragePath("gs://Bucket/path/morepath/Object"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/morepath/Object"); + + // Extra slashes: + test_path = StoragePath("gs://Bucket/path////Object"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/Object"); + + // Path with no Object: + test_path = StoragePath("gs://Bucket/path////more////"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/more"); +} + +// Test the HTTP(S)-based StoragePath constructors +TEST_F(StorageDesktopUtilsTests, testHTTPStoragePathConstructors) { + StoragePath test_path; + std::string intended_bucket_result = "Bucket"; + std::string intended_path_result = "path/to/Object/Object.data"; + + // Test basic case: + test_path = StoragePath( + "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2fto%2FObject%2fObject.data"); + EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); + EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); + + // httpS (instead of http): + test_path = StoragePath( + "https://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2fto%2FObject%2fObject.data"); + EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); + EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); + + // Extra slashes: + test_path = StoragePath( + "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2f%2f%2f%2fto%2FObject%2f%2f%2f%2fObject.data"); + EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); + EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); +} + +TEST_F(StorageDesktopUtilsTests, testInvalidConstructors) { + StoragePath bad_path("argleblargle://Bucket/path1/path2/Object"); + EXPECT_FALSE(bad_path.IsValid()); +} + +// Test the StoragePath.Parent() function. +TEST_F(StorageDesktopUtilsTests, testStoragePathParent) { + StoragePath test_path; + + // Test parent, when there is an GetObject. + test_path = StoragePath("gs://Bucket/path/Object").GetParent(); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path"); + + // Test parent with no GetObject. + test_path = StoragePath("gs://Bucket/path/morepath/").GetParent(); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path"); +} + +// Test the StoragePath.Child() function. +TEST_F(StorageDesktopUtilsTests, testStoragePathChild) { + StoragePath test_path; + + // Test child when there is no object. + test_path = StoragePath("gs://Bucket/path/morepath/").GetChild("newobj"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/morepath/newobj"); + + // Test child when there is an object. + test_path = StoragePath("gs://Bucket/path/object").GetChild("newpath/"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/object/newpath"); +} + +TEST_F(StorageDesktopUtilsTests, testUrlConverter) { + StoragePath test_path("gs://Bucket/path1/path2/Object"); + + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path1/path2/Object"); + + EXPECT_STREQ(test_path.AsHttpUrl().c_str(), + "https://firebasestorage.googleapis.com" + "/v0/b/Bucket/o/path1%2Fpath2%2FObject?alt=media"); + EXPECT_STREQ(test_path.AsHttpMetadataUrl().c_str(), + "https://firebasestorage.googleapis.com" + "/v0/b/Bucket/o/path1%2Fpath2%2FObject"); +} + +TEST_F(StorageDesktopUtilsTests, testMetadataJsonExporter) { + std::unique_ptr app(firebase::testing::CreateApp()); + std::unique_ptr storage( + new StorageInternal(app.get(), "gs://abucket")); + std::unique_ptr reference( + storage->GetReferenceFromUrl("gs://abucket/path/to/a/file.txt")); + MetadataInternal metadata(reference->AsStorageReference()); + reference.reset(nullptr); + + metadata.set_cache_control("cache_control_test"); + metadata.set_content_disposition("content_disposition_test"); + metadata.set_content_encoding("content_encoding_test"); + metadata.set_content_language("content_language_test"); + metadata.set_content_type("content_type_test"); + + std::map& custom_metadata = + *metadata.custom_metadata(); + custom_metadata["key1"] = "value1"; + custom_metadata["key2"] = "value2"; + custom_metadata["key3"] = "value3"; + + std::string json = metadata.ExportAsJson(); + + // clang-format=off + EXPECT_THAT( + json, + ::firebase::testing::cppsdk::EqualsJson( + "{\"bucket\":\"abucket\"," + "\"cacheControl\":\"cache_control_test\"," + "\"contentDisposition\":\"content_disposition_test\"," + "\"contentEncoding\":\"content_encoding_test\"," + "\"contentLanguage\":\"content_language_test\"," + "\"contentType\":\"content_type_test\"," + "\"metadata\":" + "{\"key1\":\"value1\"," + "\"key2\":\"value2\"," + "\"key3\":\"value3\"}," + "\"name\":\"file.txt\"}")); + // clang-format=on +} + +} // namespace + +int main(int argc, char** argv) { + // On Linux, add: absl::SetFlag(&FLAGS_logtostderr, true); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/testing/config_test.cc b/testing/config_test.cc new file mode 100644 index 0000000000..d71280155a --- /dev/null +++ b/testing/config_test.cc @@ -0,0 +1,174 @@ +// Copyright 2020 Google +// +// 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. + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#include +#include "testing/run_all_tests.h" +#elif defined(__APPLE__) && TARGET_OS_IPHONE +#include "testing/config_ios.h" +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) +#include "testing/config_desktop.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/testdata_config_generated.h" +#include "flatbuffers/idl.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +const constexpr int64_t kNullObject = -1; +const constexpr int64_t kException = -2; // NOLINT + +// Mimic what fake will do to get the test data provided by test user. +int64_t GetFutureBoolTicker(const char* fake) { + int64_t result; + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + + // Normally, we only send test data but not read test data in C++. Android + // fakes read test data, which is in Java code. Here we use JNI calls to + // simulate that scenario. + JNIEnv* android_jni_env = GetTestJniEnv(); + jstring jfake = android_jni_env->NewStringUTF(fake); + + jclass config_cls = android_jni_env->FindClass( + "com/google/testing/ConfigAndroid"); + jobject jrow = android_jni_env->CallStaticObjectMethod( + config_cls, + android_jni_env->GetStaticMethodID( + config_cls, "get", + "(Ljava/lang/String;)Lcom/google/testing/ConfigRow;"), + jfake); + + // Catch any Java exception and thus the test itself does not die. + if (android_jni_env->ExceptionCheck()) { + android_jni_env->ExceptionDescribe(); + android_jni_env->ExceptionClear(); + result = kException; + } else if (jrow == nullptr) { + result = kNullObject; + } else { + jclass row_cls = android_jni_env->FindClass( + "com/google/testing/ConfigRow"); + jobject jfuturebool = android_jni_env->CallObjectMethod( + jrow, android_jni_env->GetMethodID( + row_cls, "futurebool", + "()Lcom/google/testing/FutureBool;")); + EXPECT_EQ(android_jni_env->ExceptionCheck(), JNI_FALSE); + android_jni_env->ExceptionClear(); + jclass futurebool_cls = android_jni_env->FindClass( + "com/google/testing/FutureBool"); + jlong jticker = android_jni_env->CallLongMethod( + jfuturebool, + android_jni_env->GetMethodID(futurebool_cls, "ticker", "()J")); + EXPECT_EQ(android_jni_env->ExceptionCheck(), JNI_FALSE); + android_jni_env->ExceptionClear(); + + android_jni_env->DeleteLocalRef(futurebool_cls); + android_jni_env->DeleteLocalRef(jfuturebool); + android_jni_env->DeleteLocalRef(row_cls); + android_jni_env->DeleteLocalRef(jrow); + result = jticker; + } + android_jni_env->DeleteLocalRef(config_cls); + android_jni_env->DeleteLocalRef(jfake); + +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP) + + const ConfigRow* config = ConfigGet(fake); + if (config == nullptr) { + result = kNullObject; + } else { + EXPECT_EQ(fake, config->fake()->str()); + result = config->futurebool()->ticker(); + } + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + + return result; +} + +// Verify fake gets the data set by test user. +TEST(ConfigTest, TestConfigSetAndGet) { + ConfigSet( + "{" + " config:[" + " {fake:'key'," + " futurebool:{value:Error,ticker:10}}" + " ]" + "}"); + EXPECT_EQ(10, GetFutureBoolTicker("key")); +} + +// Verify fake gets provided data for multiple fake case. +TEST(ConfigTest, TestConfigSetMultipleAndGet) { + ConfigSet( + "{" + " config:[" + " {fake:'1',futurebool:{ticker:1}}," + " {fake:'7',futurebool:{ticker:7}}," + " {fake:'2',futurebool:{ticker:2}}," + " {fake:'6',futurebool:{ticker:6}}," + " {fake:'3',futurebool:{ticker:3}}," + " {fake:'5',futurebool:{ticker:5}}," + " {fake:'4',futurebool:{ticker:4}}" + " ]" + "}"); + char fake[] = {0, 0}; + for (int i = 1; i <= 7; ++i) { + fake[0] = '0' + i; + EXPECT_EQ(i, GetFutureBoolTicker(fake)); + } +} + +// Verify fake gets null if it is not specified by test user. +TEST(ConfigTest, TestConfigSetAndGetNothing) { + ConfigSet( + "{" + " config:[" + " {fake:'key'," + " futurebool:{value:False,ticker:10}}" + " ]" + "}"); + EXPECT_EQ(kNullObject, GetFutureBoolTicker("absence")); +} + +// Test the reset of test config. Nothing to verify except to make sure code +// nothing is not broken. +TEST(ConfigTest, TestConfigReset) { + ConfigSet("{}"); + ConfigReset(); +} + +// Verify exception raises when access the unset config. +TEST(ConfigDeathTest, TestConfigResetAndGet) { + ConfigSet("{}"); + ConfigReset(); +// Somehow the death test does not work on android emulator nor ios emulator. +#if !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE) + EXPECT_DEATH(GetFutureBoolTicker("absence"), ""); +#endif // !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE) +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/reporter_impl_fake.cc b/testing/reporter_impl_fake.cc new file mode 100644 index 0000000000..e8e125120f --- /dev/null +++ b/testing/reporter_impl_fake.cc @@ -0,0 +1,34 @@ +// Copyright 2020 Google +// +// 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 "testing/reporter_impl_fake.h" + +#include "testing/reporter_impl.h" + +namespace firebase { +namespace testing { +namespace cppsdk { +namespace fake { + +void TestFunction() { + FakeReporter->AddReport( + "fake_function_name", "fake_function_result", + std::initializer_list({ + "fake_argument0", "fake_argument1", "fake_argument2"})); +} + +} // namespace fake +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/reporter_impl_test.cc b/testing/reporter_impl_test.cc new file mode 100644 index 0000000000..bce4496651 --- /dev/null +++ b/testing/reporter_impl_test.cc @@ -0,0 +1,45 @@ +// Copyright 2020 Google +// +// 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 "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/reporter.h" +#include "testing/reporter_impl_fake.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +class ReporterImplTest : public ::testing::Test { + protected: + void SetUp() override { reporter_.reset(); } + + void TearDown() override { + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + Reporter reporter_; +}; + +TEST_F(ReporterImplTest, Test) { + reporter_.addExpectation( + "fake_function_name", "fake_function_result", kAny, + {"fake_argument0", "fake_argument1", "fake_argument2"}); + fake::TestFunction(); +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/reporter_test.cc b/testing/reporter_test.cc new file mode 100644 index 0000000000..0a87863113 --- /dev/null +++ b/testing/reporter_test.cc @@ -0,0 +1,190 @@ +// Copyright 2020 Google +// +// 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 + +#include "testing/reporter.h" +#include "testing/run_all_tests.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +TEST(ReportRowTest, TestGetFake) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getFake(), "fake"); +} + +TEST(ReportRowTest, TestGetResult) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getResult(), "result"); +} + +TEST(ReportRowTest, TestGetArgs) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_THAT(r.getArgs(), + ::testing::Eq(std::vector{"1", "2", "3"})); +} + +TEST(ReportRowTest, TestGetPlatform) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kAny); + + r = ReportRow("fake", "result", kAndroid, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kAndroid); + + r = ReportRow("fake", "result", kIos, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kIos); + + r = ReportRow("fake", "result", {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kAny); +} + +TEST(ReportRowTest, TestGetPlatformString) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "any"); + + r = ReportRow("fake", "result", kAndroid, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "android"); + + r = ReportRow("fake", "result", kIos, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "ios"); + + r = ReportRow("fake", "result", {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "any"); +} + +TEST(ReportRowTest, TestToString) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.toString(), "fake result any [1 2 3]"); + + r = ReportRow("", "", kAny, {}); + EXPECT_EQ(r.toString(), " any []"); +} + +// Compare only fake_ values +TEST(ReportRowTest, TestLessThanOperator) { + ReportRow r1("abc", "9876", kAny, {"a", "a", "a"}); + ReportRow r2("xyz", "5555", kAny, {"x", "x", "x"}); + + EXPECT_TRUE(r1 < r2); + EXPECT_FALSE(r2 < r1); + + EXPECT_FALSE(r1 < r1); + EXPECT_FALSE(r2 < r2); +} + +TEST(ReportRowTest, TestEqualOperator) { + ReportRow r1("abc", "9876", kAny, {"a", "a", "a"}); + ReportRow r2("xyz", "5555", kAny, {"x", "x", "x"}); + ReportRow r3("xyz", "4444", kAny, {"z", "z", "z"}); + + EXPECT_FALSE(r1 == r2); + EXPECT_FALSE(r2 == r1); + + EXPECT_TRUE(r1 == r1); + EXPECT_TRUE(r2 == r2); + + EXPECT_FALSE(r2 == r3); +} + +TEST(ReportRowTest, TestNotEqualOperator) { + ReportRow r1("abc", "9876", kAny, {"a", "a", "a"}); + ReportRow r2("xyz", "5555", kAny, {"x", "x", "x"}); + ReportRow r3("xyz", "4444", kAny, {"z", "z", "z"}); + + EXPECT_TRUE(r1 != r2); + EXPECT_TRUE(r2 != r1); + + EXPECT_FALSE(r1 != r1); + EXPECT_FALSE(r2 != r2); + + EXPECT_TRUE(r2 != r3); +} + +class ReporterTest : public ::testing::Test { + protected: + Reporter r_; +}; + +TEST_F(ReporterTest, TestGetExpectations) { + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + r_.addExpectation("fake2", "result2", kAny, {"one", "two"}); + r_.addExpectation(ReportRow("fake3", "result3", kAny, {"one", "two"})); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"}), + ReportRow("fake2", "result2", kAny, {"one", "two"}), + ReportRow("fake3", "result3", kAny, {"one", "two"})})); +} + +TEST_F(ReporterTest, TestGetExpectationsSortedByKey) { + r_.addExpectation(ReportRow("fake3", "result3", kAny, {"one", "two"})); + r_.addExpectation("fake2", "result2", kAny, {"one", "two"}); + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"}), + ReportRow("fake2", "result2", kAny, {"one", "two"}), + ReportRow("fake3", "result3", kAny, {"one", "two"})})); +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || defined(__ANDROID__) +TEST_F(ReporterTest, TestGetExpectationsAndroid) { + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + r_.addExpectation("fake2", "result2", kAndroid, {"one", "two"}); + r_.addExpectation(ReportRow("fake3", "result3", kIos, {"one", "two"})); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"}), + ReportRow("fake2", "result2", kAndroid, {"one", "two"})})); +} + +TEST_F(ReporterTest, TestResetAndroid) { + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"})})); + r_.reset(); + EXPECT_THAT(r_.getExpectations(), ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetFakeReportsAndroid) { + EXPECT_THAT(r_.getFakeReports(), ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetAllFakesAndroid) { + EXPECT_THAT(r_.getAllFakes(), ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetFakeArgsAndroid) { + EXPECT_THAT(r_.getFakeArgs("some_fake"), + ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetFakeResultAndroid) { + EXPECT_EQ(r_.getFakeResult("some_fake"), ""); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || defined(__ANDROID__) + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/ticker_test.cc b/testing/ticker_test.cc new file mode 100644 index 0000000000..e6c6238ea5 --- /dev/null +++ b/testing/ticker_test.cc @@ -0,0 +1,170 @@ +// Copyright 2020 Google +// +// 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. + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#include +#include "testing/run_all_tests.h" +#elif defined(__APPLE__) && TARGET_OS_IPHONE +#include "testing/ticker_ios.h" +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) +#include "testing/ticker_desktop.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/ticker.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +// We count the status change by this. +static int g_status_count = 0; + +// Now we define example ticker class. TickerObserver is abstract and we cannot +// test it directly. Generally speaking, fakes mimic callbacks by inheriting +// TickerObserver class and overriding Update() method. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +extern "C" JNIEXPORT void JNICALL +Java_com_google_firebase_testing_cppsdk_TickerExample_nativeFunction( + JNIEnv* env, jobject this_obj, jlong ticker, jlong delay) { + if (ticker == delay) { + ++g_status_count; + } +} + +class Tickers { + public: + Tickers(std::initializer_list delays) { + JNIEnv* android_jni_env = GetTestJniEnv(); + jclass class_obj = android_jni_env->FindClass( + "com/google/testing/TickerExample"); + jmethodID methid_id = + android_jni_env->GetMethodID(class_obj, "", "(J)V"); + for (int64_t delay : delays) { + jobject observer = + android_jni_env->NewObject(class_obj, methid_id, delay); + android_jni_env->DeleteLocalRef(observer); + } + android_jni_env->DeleteLocalRef(class_obj); + } +}; +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP) +class TickerExample : public TickerObserver { + public: + explicit TickerExample(int64_t delay) : delay_(delay) { + RegisterTicker(this); + } + + void Elapse() override { + if (TickerNow() == delay_) { + ++g_status_count; + } + } + + private: + // When the callback should happen. + const int64_t delay_; +}; + +class Tickers { + public: + Tickers(std::initializer_list delays) { + for (int64_t delay : delays) { + tickers_.push_back( + std::shared_ptr(new TickerExample(delay))); + } + } + + private: + std::vector > tickers_; +}; +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +class TickerTest : public ::testing::Test { + protected: + void SetUp() override { g_status_count = 0; } + void TearDown() override { TickerReset(); } +}; + +// This test make sure nothing is broken by calling a sequence of elapse and +// reset. Since there is no observer, we do not have anything to verify yet. +TEST_F(TickerTest, TestNoObserver) { + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + + TickerReset(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); +} + +// Test one observer that changes status immediately. +TEST_F(TickerTest, TestObserverCallbackImmediate) { + Tickers tickers({0L}); + + // Now verify the status changed immediately. + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); +} + +// Test one observer that changes status after two tickers. +TEST_F(TickerTest, TestObserverDelayTwo) { + Tickers tickers({2L}); + + // Now start the ticker and verify. + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); +} + +// Test two observers that changes status after one, respectively, two tickers. +TEST_F(TickerTest, TestMultipleObservers) { + Tickers tickers({1L, 2L}); + + // Now start the ticker and verify. + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(2, g_status_count); + TickerElapse(); + EXPECT_EQ(2, g_status_count); +} +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/util_android_test.cc b/testing/util_android_test.cc new file mode 100644 index 0000000000..6314868576 --- /dev/null +++ b/testing/util_android_test.cc @@ -0,0 +1,86 @@ +// Copyright 2020 Google +// +// 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 +#include + +#include "testing/run_all_tests.h" +#include "testing/util_android.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +class UtilTest : public ::testing::Test { + protected: + JNIEnv* env_ = GetTestJniEnv(); +}; + +TEST_F(UtilTest, JavaStringToString) { + jstring java_string = env_->NewStringUTF("hello world"); + std::string cc_string = util::JavaStringToStdString(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_EQ(cc_string, "hello world"); +} + +TEST_F(UtilTest, JavaStringToStringWithEmptyJavaString) { + jstring java_string = env_->NewStringUTF(nullptr); + std::string cc_string = util::JavaStringToStdString(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_EQ(cc_string, ""); +} + +TEST_F(UtilTest, JavaStringListToStdStringVector) { + std::vector arr = {"one", "two", "three", "four", "five"}; + + jclass jarray_list_class = env_->FindClass("java/util/ArrayList"); + jobject jarray_list = env_->NewObject( + jarray_list_class, env_->GetMethodID(jarray_list_class, "", "()V")); + + for (const std::string& s : arr) { + jstring java_string = env_->NewStringUTF(s.c_str()); + env_->CallBooleanMethod( + jarray_list, + env_->GetMethodID(jarray_list_class, "add", "(Ljava/lang/Object;)Z"), + java_string); + util::CheckAndClearException(env_); + env_->DeleteLocalRef(java_string); + } + + EXPECT_THAT(util::JavaStringListToStdStringVector(env_, jarray_list), + ::testing::Eq(arr)); + + env_->DeleteLocalRef(jarray_list_class); + env_->DeleteLocalRef(jarray_list); +} + +TEST_F(UtilTest, JavaStringListToStdStringVectorWithEmptyJavaList) { + jclass jarray_list_class = env_->FindClass("java/util/ArrayList"); + jobject jarray_list = env_->NewObject( + jarray_list_class, env_->GetMethodID(jarray_list_class, "", "()V")); + + EXPECT_THAT(util::JavaStringListToStdStringVector(env_, jarray_list), + ::testing::Eq(std::vector())); + + env_->DeleteLocalRef(jarray_list_class); + env_->DeleteLocalRef(jarray_list); +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/util_ios_test.mm b/testing/util_ios_test.mm new file mode 100644 index 0000000000..05022f5a3a --- /dev/null +++ b/testing/util_ios_test.mm @@ -0,0 +1,80 @@ +// Copyright 2020 Google +// +// 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. + +#import + +#include + +#include "testing/config.h" +#include "testing/ticker.h" +#include "testing/util_ios.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +TEST(TickerTest, TestCallbackTicker) { + TickerReset(); + ConfigSet( + "{" + " config:[" + " {fake:'a',futuregeneric:{throwexception:false,ticker:1}}," + " {fake:'b',futuregeneric:{throwexception:true,exceptionmsg:'failed',ticker:2}}," + " {fake:'c',futuregeneric:{throwexception:false,ticker:3}}," + " {fake:'d',futuregeneric:{throwexception:true,exceptionmsg:'failed',ticker:4}}" + " ]" + "}"); + + __block int count = 0; + // Now we create four fake objects on the fly; all are managed by manager. + CallbackTickerManager manager; + // Without param. + manager.Add(@"a", ^(NSError* _Nullable error) { if (!error) count++; }); + manager.Add(@"b", ^(NSError* _Nullable error) { if (!error) count++; }); + // With param. + manager.Add(@"c", ^(NSString* param, NSError* _Nullable error) { if (!error) count++; }, @"par"); + manager.Add(@"d", ^(NSString* param, NSError* _Nullable error) { if (!error) count++; }, @"par"); + + // nothing happens so far. + EXPECT_EQ(0, count); + + // a succeeds and increases counter. + TickerElapse(); + EXPECT_EQ(1, count); + + // b fails. + TickerElapse(); + EXPECT_EQ(1, count); + + // c succeeds and increases counter. + TickerElapse(); + EXPECT_EQ(2, count); + + // d fails. + TickerElapse(); + EXPECT_EQ(2, count); + + // nothing happens afterwards. + TickerElapse(); + EXPECT_EQ(2, count); + + TickerReset(); + ConfigReset(); +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/version_header_test.py b/version_header_test.py new file mode 100644 index 0000000000..82e7ce4d91 --- /dev/null +++ b/version_header_test.py @@ -0,0 +1,103 @@ +# Copyright 2018 Google LLC +# +# 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. + +"""Tests for google3.firebase.app.client.cpp.version_header.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from google3.testing.pybase import googletest +from google3.firebase.app.client.cpp import version_header + +EXPECTED_VERSION_HEADER = r"""// Copyright 2016 Google Inc. All Rights Reserved. + +#ifndef FIREBASE_APP_CLIENT_CPP_SRC_VERSION_H_ +#define FIREBASE_APP_CLIENT_CPP_SRC_VERSION_H_ + +/// @def FIREBASE_VERSION_MAJOR +/// @brief Major version number of the Firebase C++ SDK. +/// @see kFirebaseVersionString +#define FIREBASE_VERSION_MAJOR 1 +/// @def FIREBASE_VERSION_MINOR +/// @brief Minor version number of the Firebase C++ SDK. +/// @see kFirebaseVersionString +#define FIREBASE_VERSION_MINOR 2 +/// @def FIREBASE_VERSION_REVISION +/// @brief Revision number of the Firebase C++ SDK. +/// @see kFirebaseVersionString +#define FIREBASE_VERSION_REVISION 3 + +/// @cond FIREBASE_APP_INTERNAL +#define FIREBASE_STRING_EXPAND(X) #X +#define FIREBASE_STRING(X) FIREBASE_STRING_EXPAND(X) +/// @endcond + +// Version number. +// clang-format off +#define FIREBASE_VERSION_NUMBER_STRING \ + FIREBASE_STRING(FIREBASE_VERSION_MAJOR) "." \ + FIREBASE_STRING(FIREBASE_VERSION_MINOR) "." \ + FIREBASE_STRING(FIREBASE_VERSION_REVISION) +// clang-format on + +// Identifier for version string, e.g. kFirebaseVersionString. +#define FIREBASE_VERSION_IDENTIFIER(library) k##library##VersionString + +// Concatenated version string, e.g. "Firebase C++ x.y.z". +#define FIREBASE_VERSION_STRING(library) \ + #library " C++ " FIREBASE_VERSION_NUMBER_STRING + +#if !defined(DOXYGEN) +#if !defined(_WIN32) && !defined(__CYGWIN__) +#define DEFINE_FIREBASE_VERSION_STRING(library) \ + extern volatile __attribute__((weak)) \ + const char* FIREBASE_VERSION_IDENTIFIER(library); \ + volatile __attribute__((weak)) \ + const char* FIREBASE_VERSION_IDENTIFIER(library) = \ + FIREBASE_VERSION_STRING(library) +#else +#define DEFINE_FIREBASE_VERSION_STRING(library) \ + static const char* FIREBASE_VERSION_IDENTIFIER(library) = \ + FIREBASE_VERSION_STRING(library) +#endif // !defined(_WIN32) && !defined(__CYGWIN__) +#else // if defined(DOXYGEN) + +/// @brief Namespace that encompasses all Firebase APIs. +namespace firebase { + +/// @brief String which identifies the current version of the Firebase C++ +/// SDK. +/// +/// @see FIREBASE_VERSION_MAJOR +/// @see FIREBASE_VERSION_MINOR +/// @see FIREBASE_VERSION_REVISION +static const char* kFirebaseVersionString = FIREBASE_VERSION_STRING; + +} // namespace firebase +#endif // !defined(DOXYGEN) + +#endif // FIREBASE_APP_CLIENT_CPP_SRC_VERSION_H_ +""" + + +class VersionHeaderGeneratorTest(googletest.TestCase): + + def test_generate_header(self): + result_header = version_header.generate_header(1, 2, 3) + self.assertEqual(result_header, EXPECTED_VERSION_HEADER) + + +if __name__ == '__main__': + googletest.main() From 598f485ed36804e9f5066941cfd1c01e43d0d3ad Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 18 Jun 2020 11:14:43 -0700 Subject: [PATCH 006/109] Add unit tests for the Firebase libraries PiperOrigin-RevId: 317141369 --- admob/tools/ios/testapp/README.md | 57 + admob/tools/ios/testapp/p4_depot_paths | 7 + .../testapp/testapp.xcodeproj/project.pbxproj | 524 ++++ admob/tools/ios/testapp/testapp/AppDelegate.h | 11 + admob/tools/ios/testapp/testapp/AppDelegate.m | 16 + .../AppIcon.appiconset/Contents.json | 73 + .../testapp/Assets.xcassets/Contents.json | 6 + .../LaunchImages.launchimage/Contents.json | 36 + .../Base.lproj/LaunchScreen.storyboard | 27 + .../testapp/Base.lproj/Main.storyboard | 27 + admob/tools/ios/testapp/testapp/Info.plist | 50 + .../ios/testapp/testapp/ViewController.h | 8 + .../ios/testapp/testapp/ViewController.mm | 135 + .../tools/ios/testapp/testapp/game_engine.cpp | 551 ++++ admob/tools/ios/testapp/testapp/game_engine.h | 74 + admob/tools/ios/testapp/testapp/main.m | 11 + analytics/generate_constants_test.py | 176 ++ analytics/src_ios/fake/FIRAnalytics.h | 39 + analytics/src_ios/fake/FIRAnalytics.mm | 94 + .../firebase/analytics/FirebaseAnalytics.java | 91 + analytics/tests/CMakeLists.txt | 41 + analytics/tests/analytics_test.cc | 310 ++ .../instance_id_desktop_impl_test.cc | 819 +++++ app/memory/atomic_test.cc | 99 + app/memory/shared_ptr_test.cc | 229 ++ app/memory/unique_ptr_test.cc | 174 ++ app/meta/move_test.cc | 68 + app/rest/tests/gzipheader_unittest.cc | 162 + app/rest/tests/request_binary_test.cc | 96 + app/rest/tests/request_file_test.cc | 96 + app/rest/tests/request_json_test.cc | 71 + app/rest/tests/request_test.cc | 57 + app/rest/tests/request_test.h | 116 + app/rest/tests/response_binary_test.cc | 128 + app/rest/tests/response_json_test.cc | 130 + app/rest/tests/response_test.cc | 101 + app/rest/tests/testdata/sample.fbs | 24 + app/rest/tests/transport_curl_test.cc | 180 ++ app/rest/tests/transport_mock_test.cc | 75 + app/rest/tests/util_test.cc | 60 + app/rest/tests/www_form_url_encoded_test.cc | 107 + app/rest/tests/zlibwrapper_unittest.cc | 1050 +++++++ app/src/fake/FIRApp.h | 58 + app/src/fake/FIRApp.mm | 106 + app/src/fake/FIRConfiguration.h | 45 + app/src/fake/FIRConfiguration.m | 34 + app/src/fake/FIRLogger.h | 34 + app/src/fake/FIRLoggerLevel.h | 38 + app/src/fake/FIROptions.h | 51 + app/src/fake/FIROptions.mm | 66 + .../gms/common/GoogleApiAvailability.java | 48 + .../com/google/android/gms/tasks/Task.java | 114 + .../fake/com/google/firebase/FirebaseApp.java | 76 + .../google/firebase/FirebaseException.java | 25 + .../com/google/firebase/FirebaseOptions.java | 133 + .../app/internal/cpp/CppThreadDispatcher.java | 48 + .../cpp/GoogleApiAvailabilityHelper.java | 57 + .../app/internal/cpp/JniResultCallback.java | 103 + .../GlobalLibraryVersionRegistrar.java | 36 + app/tests/CMakeLists.txt | 410 +++ app/tests/app_test.cc | 599 ++++ app/tests/assert_test.cc | 283 ++ app/tests/base64_openssh_test.cc | 98 + app/tests/base64_test.cc | 221 ++ app/tests/callback_test.cc | 462 +++ app/tests/cleanup_notifier_test.cc | 417 +++ app/tests/flexbuffer_matcher.cc | 252 ++ app/tests/flexbuffer_matcher.h | 56 + app/tests/flexbuffer_matcher_test.cc | 236 ++ app/tests/future_manager_test.cc | 205 ++ app/tests/future_test.cc | 1567 ++++++++++ .../availability_android_test.cc | 242 ++ app/tests/google_services_test.cc | 75 + app/tests/include/firebase/app_for_testing.h | 59 + app/tests/intrusive_list_test.cc | 1229 ++++++++ app/tests/jobject_reference_test.cc | 162 + app/tests/locale_test.cc | 56 + app/tests/log_test.cc | 55 + app/tests/logger_test.cc | 283 ++ app/tests/optional_test.cc | 446 +++ app/tests/path_test.cc | 452 +++ app/tests/reference_count_test.cc | 275 ++ app/tests/scheduler_test.cc | 369 +++ .../secure/user_secure_integration_test.cc | 255 ++ app/tests/secure/user_secure_internal_test.cc | 285 ++ app/tests/secure/user_secure_manager_test.cc | 178 ++ app/tests/semaphore_test.cc | 96 + app/tests/swizzle_test.mm | 131 + app/tests/thread_test.cc | 194 ++ app/tests/time_test.cc | 101 + app/tests/util_android_test.cc | 479 +++ app/tests/util_ios_test.mm | 650 ++++ app/tests/uuid_test.cc | 42 + app/tests/variant_test.cc | 1186 +++++++ app/tests/variant_util_test.cc | 549 ++++ auth/src/ios/fake/FIRActionCodeSettings.h | 89 + auth/src/ios/fake/FIRAdditionalUserInfo.h | 61 + auth/src/ios/fake/FIRAdditionalUserInfo.mm | 35 + auth/src/ios/fake/FIRAuth.h | 832 +++++ auth/src/ios/fake/FIRAuth.mm | 214 ++ auth/src/ios/fake/FIRAuthAPNSTokenType.h | 40 + auth/src/ios/fake/FIRAuthCredential.h | 44 + auth/src/ios/fake/FIRAuthCredential.mm | 33 + auth/src/ios/fake/FIRAuthDataResult.h | 61 + auth/src/ios/fake/FIRAuthDataResult.mm | 37 + auth/src/ios/fake/FIRAuthErrors.h | 358 +++ auth/src/ios/fake/FIRAuthSettings.h | 35 + auth/src/ios/fake/FIRAuthTokenResult.h | 66 + auth/src/ios/fake/FIRAuthUIDelegate.h | 53 + auth/src/ios/fake/FIREmailAuthProvider.h | 70 + auth/src/ios/fake/FIREmailAuthProvider.mm | 35 + auth/src/ios/fake/FIRFacebookAuthProvider.h | 54 + auth/src/ios/fake/FIRFacebookAuthProvider.mm | 30 + auth/src/ios/fake/FIRFederatedAuthProvider.h | 52 + auth/src/ios/fake/FIRGameCenterAuthProvider.h | 62 + .../src/ios/fake/FIRGameCenterAuthProvider.mm | 27 + auth/src/ios/fake/FIRGitHubAuthProvider.h | 55 + auth/src/ios/fake/FIRGitHubAuthProvider.mm | 31 + auth/src/ios/fake/FIRGoogleAuthProvider.h | 56 + auth/src/ios/fake/FIRGoogleAuthProvider.mm | 32 + auth/src/ios/fake/FIROAuthCredential.h | 55 + auth/src/ios/fake/FIROAuthCredential.mm | 35 + auth/src/ios/fake/FIROAuthProvider.h | 113 + auth/src/ios/fake/FIROAuthProvider.mm | 68 + auth/src/ios/fake/FIRPhoneAuthCredential.h | 38 + auth/src/ios/fake/FIRPhoneAuthCredential.mm | 35 + auth/src/ios/fake/FIRPhoneAuthProvider.h | 109 + auth/src/ios/fake/FIRPhoneAuthProvider.mm | 49 + auth/src/ios/fake/FIRTwitterAuthProvider.h | 54 + auth/src/ios/fake/FIRTwitterAuthProvider.mm | 31 + auth/src/ios/fake/FIRUser.h | 507 +++ auth/src/ios/fake/FIRUser.mm | 161 + auth/src/ios/fake/FIRUserInfo.h | 60 + auth/src/ios/fake/FIRUserMetadata.h | 49 + auth/src/ios/fake/FIRUserMetadata.mm | 34 + auth/src/ios/fake/FirebaseAuth.h | 46 + auth/src/ios/fake/FirebaseAuthVersion.h | 27 + .../FirebaseApiNotAvailableException.java | 25 + .../firebase/FirebaseNetworkException.java | 25 + .../FirebaseTooManyRequestsException.java | 25 + .../firebase/auth/AdditionalUserInfo.java | 35 + .../google/firebase/auth/AuthCredential.java | 32 + .../com/google/firebase/auth/AuthResult.java | 29 + .../firebase/auth/EmailAuthProvider.java | 25 + .../firebase/auth/FacebookAuthProvider.java | 25 + .../firebase/auth/FederatedAuthProvider.java | 22 + .../google/firebase/auth/FirebaseAuth.java | 242 ++ .../auth/FirebaseAuthActionCodeException.java | 25 + .../auth/FirebaseAuthEmailException.java | 25 + .../firebase/auth/FirebaseAuthException.java | 34 + ...rebaseAuthInvalidCredentialsException.java | 25 + .../FirebaseAuthInvalidUserException.java | 25 + ...ebaseAuthRecentLoginRequiredException.java | 25 + .../FirebaseAuthUserCollisionException.java | 25 + .../FirebaseAuthWeakPasswordException.java | 29 + .../auth/FirebaseAuthWebException.java | 25 + .../google/firebase/auth/FirebaseUser.java | 201 ++ .../firebase/auth/FirebaseUserMetadata.java | 31 + .../google/firebase/auth/GetTokenResult.java | 25 + .../firebase/auth/GithubAuthProvider.java | 25 + .../firebase/auth/GoogleAuthProvider.java | 25 + .../google/firebase/auth/OAuthProvider.java | 138 + .../firebase/auth/PhoneAuthCredential.java | 24 + .../firebase/auth/PhoneAuthProvider.java | 56 + .../firebase/auth/PlayGamesAuthProvider.java | 25 + .../auth/SignInMethodQueryResult.java | 27 + .../firebase/auth/TwitterAuthProvider.java | 25 + .../com/google/firebase/auth/UserInfo.java | 53 + .../auth/UserProfileChangeRequest.java | 38 + auth/tests/CMakeLists.txt | 282 ++ auth/tests/auth_test.cc | 558 ++++ auth/tests/credential_test.cc | 113 + auth/tests/desktop/auth_desktop_test.cc | 895 ++++++ auth/tests/desktop/fakes.cc | 118 + auth/tests/desktop/fakes.h | 64 + .../desktop/rpcs/create_auth_uri_test.cc | 66 + .../tests/desktop/rpcs/delete_account_test.cc | 57 + .../desktop/rpcs/get_account_info_test.cc | 81 + .../rpcs/get_oob_confirmation_code_test.cc | 81 + .../tests/desktop/rpcs/reset_password_test.cc | 61 + auth/tests/desktop/rpcs/secure_token_test.cc | 68 + .../desktop/rpcs/set_account_info_test.cc | 173 + .../desktop/rpcs/sign_up_new_user_test.cc | 110 + auth/tests/desktop/rpcs/test_util.cc | 69 + auth/tests/desktop/rpcs/test_util.h | 39 + .../desktop/rpcs/verify_assertion_test.cc | 87 + .../desktop/rpcs/verify_custom_token_test.cc | 90 + .../desktop/rpcs/verify_password_test.cc | 105 + auth/tests/desktop/test_utils.cc | 71 + auth/tests/desktop/test_utils.h | 295 ++ auth/tests/desktop/user_desktop_test.cc | 1217 ++++++++ auth/tests/user_test.cc | 520 +++ binary_to_array_test.py | 91 + build_type_header_test.py | 46 + database/src/ios/util_ios_test.mm | 111 + database/tests/CMakeLists.txt | 343 ++ .../tests/common/database_reference_test.cc | 283 ++ .../desktop/connection/connection_test.cc | 301 ++ .../connection/web_socket_client_impl_test.cc | 268 ++ .../tests/desktop/core/cache_policy_test.cc | 70 + .../tests/desktop/core/compound_write_test.cc | 545 ++++ .../desktop/core/event_registration_test.cc | 186 ++ .../desktop/core/indexed_variant_test.cc | 677 ++++ database/tests/desktop/core/operation_test.cc | 424 +++ .../tests/desktop/core/server_values_test.cc | 221 ++ .../desktop/core/sparse_snapshot_tree_test.cc | 116 + .../tests/desktop/core/sync_point_test.cc | 390 +++ database/tests/desktop/core/sync_tree_test.cc | 825 +++++ .../core/tracked_query_manager_test.cc | 396 +++ database/tests/desktop/core/tree_test.cc | 1009 ++++++ .../tests/desktop/core/write_tree_test.cc | 792 +++++ .../desktop/mutable_data_desktop_test.cc | 237 ++ .../flatbuffer_conversions_test.cc | 458 +++ ..._memory_persistence_storage_engine_test.cc | 415 +++ ...evel_db_persistence_storage_engine_test.cc | 700 +++++ .../noop_persistence_manager_test.cc | 86 + .../persistence/persistence_manager_test.cc | 461 +++ .../desktop/persistence/prune_forest_test.cc | 485 +++ .../desktop/push_child_name_generator_test.cc | 87 + database/tests/desktop/test/matchers.h | 40 + database/tests/desktop/test/matchers_test.cc | 73 + .../tests/desktop/test/mock_cache_policy.h | 45 + .../tests/desktop/test/mock_listen_provider.h | 40 + database/tests/desktop/test/mock_listener.h | 55 + .../desktop/test/mock_persistence_manager.h | 77 + .../test/mock_persistence_storage_engine.h | 79 + .../desktop/test/mock_tracked_query_manager.h | 52 + database/tests/desktop/test/mock_write_tree.h | 80 + database/tests/desktop/util_desktop_test.cc | 2775 +++++++++++++++++ database/tests/desktop/view/change_test.cc | 341 ++ .../view/child_change_accumulator_test.cc | 182 ++ .../desktop/view/event_generator_test.cc | 346 ++ .../tests/desktop/view/indexed_filter_test.cc | 391 +++ .../tests/desktop/view/limited_filter_test.cc | 314 ++ .../tests/desktop/view/ranged_filter_test.cc | 673 ++++ .../tests/desktop/view/view_cache_test.cc | 133 + .../tests/desktop/view/view_processor_test.cc | 727 +++++ database/tests/desktop/view/view_test.cc | 464 +++ firestore/generate_android_test.py | 88 + .../tests/android/field_path_portable_test.cc | 140 + ...irebase_firestore_settings_android_test.cc | 52 + .../tests/android/geo_point_android_test.cc | 24 + .../android/snapshot_metadata_android_test.cc | 42 + .../tests/android/timestamp_android_test.cc | 26 + firestore/src/tests/array_transform_test.cc | 230 ++ firestore/src/tests/cleanup_test.cc | 420 +++ .../src/tests/collection_reference_test.cc | 34 + firestore/src/tests/cursor_test.cc | 278 ++ firestore/src/tests/document_change_test.cc | 31 + .../src/tests/document_reference_test.cc | 34 + firestore/src/tests/document_snapshot_test.cc | 35 + firestore/src/tests/field_value_test.cc | 331 ++ firestore/src/tests/fields_test.cc | 229 ++ .../src/tests/firestore_integration_test.cc | 219 ++ .../src/tests/firestore_integration_test.h | 275 ++ firestore/src/tests/firestore_test.cc | 1334 ++++++++ firestore/src/tests/includes_test.cc | 87 + .../src/tests/listener_registration_test.cc | 185 ++ .../src/tests/numeric_transforms_test.cc | 204 ++ firestore/src/tests/query_network_test.cc | 148 + firestore/src/tests/query_snapshot_test.cc | 34 + firestore/src/tests/query_test.cc | 697 +++++ firestore/src/tests/sanity_test.cc | 38 + firestore/src/tests/server_timestamp_test.cc | 294 ++ firestore/src/tests/smoke_test.cc | 165 + firestore/src/tests/transaction_extra_test.cc | 114 + firestore/src/tests/transaction_test.cc | 750 +++++ firestore/src/tests/type_test.cc | 71 + .../src/tests/util/integration_test_util.cc | 66 + .../tests/util/integration_test_util_apple.mm | 21 + firestore/src/tests/validation_test.cc | 885 ++++++ firestore/src/tests/write_batch_test.cc | 314 ++ instance_id/src_ios/fake/FIRInstanceID.h | 330 ++ instance_id/src_ios/fake/FIRInstanceID.mm | 158 + .../firebase/iid/FirebaseInstanceId.java | 187 ++ instance_id/tests/CMakeLists.txt | 36 + instance_id/tests/instance_id_test.cc | 546 ++++ .../MessageForwardingServiceTest.java | 82 + .../firebase/messaging/RemoteMessageUtil.java | 31 + .../messaging/cpp/ListenerServiceTest.java | 86 + .../messaging/cpp/MessageWriterTest.java | 91 + messaging/src/ios/fake/FIRMessaging.h | 507 +++ messaging/src/ios/fake/FIRMessaging.mm | 125 + .../firebase/messaging/FirebaseMessaging.java | 81 + .../firebase/messaging/RemoteMessage.java | 73 + .../cpp/RegistrationIntentService.java | 21 + messaging/tests/CMakeLists.txt | 78 + .../tests/android/cpp/message_reader_test.cc | 289 ++ .../tests/android/cpp/messaging_test_util.cc | 277 ++ messaging/tests/ios/messaging_test_util.mm | 99 + messaging/tests/messaging_test.cc | 380 +++ messaging/tests/messaging_test_util.h | 51 + remote_config/src/desktop/rest_fake.cc | 73 + .../remoteconfig/FirebaseRemoteConfig.java | 269 ++ ...seRemoteConfigFetchThrottledException.java | 24 + .../FirebaseRemoteConfigInfo.java | 44 + .../FirebaseRemoteConfigSettings.java | 45 + .../FirebaseRemoteConfigValue.java | 72 + remote_config/tests/CMakeLists.txt | 37 + .../tests/desktop/config_data_test.cc | 156 + .../tests/desktop/file_manager_test.cc | 66 + remote_config/tests/desktop/metadata_test.cc | 101 + .../desktop/notification_channel_test.cc | 79 + .../desktop/remote_config_desktop_test.cc | 528 ++++ remote_config/tests/desktop/rest_test.cc | 502 +++ remote_config/tests/remote_config_test.cc | 692 ++++ storage/src/common/storage_uri_parser_test.cc | 153 + storage/tests/CMakeLists.txt | 25 + .../desktop/storage_desktop_utils_tests.cc | 195 ++ testing/config_test.cc | 174 ++ testing/reporter_impl_fake.cc | 34 + testing/reporter_impl_test.cc | 45 + testing/reporter_test.cc | 190 ++ testing/ticker_test.cc | 170 + testing/util_android_test.cc | 86 + testing/util_ios_test.mm | 80 + version_header_test.py | 103 + 317 files changed, 62778 insertions(+) create mode 100644 admob/tools/ios/testapp/README.md create mode 100644 admob/tools/ios/testapp/p4_depot_paths create mode 100644 admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj create mode 100644 admob/tools/ios/testapp/testapp/AppDelegate.h create mode 100644 admob/tools/ios/testapp/testapp/AppDelegate.m create mode 100644 admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json create mode 100644 admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json create mode 100644 admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard create mode 100644 admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard create mode 100644 admob/tools/ios/testapp/testapp/Info.plist create mode 100644 admob/tools/ios/testapp/testapp/ViewController.h create mode 100644 admob/tools/ios/testapp/testapp/ViewController.mm create mode 100644 admob/tools/ios/testapp/testapp/game_engine.cpp create mode 100644 admob/tools/ios/testapp/testapp/game_engine.h create mode 100644 admob/tools/ios/testapp/testapp/main.m create mode 100644 analytics/generate_constants_test.py create mode 100644 analytics/src_ios/fake/FIRAnalytics.h create mode 100644 analytics/src_ios/fake/FIRAnalytics.mm create mode 100644 analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java create mode 100644 analytics/tests/CMakeLists.txt create mode 100644 analytics/tests/analytics_test.cc create mode 100644 app/instance_id/instance_id_desktop_impl_test.cc create mode 100644 app/memory/atomic_test.cc create mode 100644 app/memory/shared_ptr_test.cc create mode 100644 app/memory/unique_ptr_test.cc create mode 100644 app/meta/move_test.cc create mode 100644 app/rest/tests/gzipheader_unittest.cc create mode 100644 app/rest/tests/request_binary_test.cc create mode 100644 app/rest/tests/request_file_test.cc create mode 100644 app/rest/tests/request_json_test.cc create mode 100644 app/rest/tests/request_test.cc create mode 100644 app/rest/tests/request_test.h create mode 100644 app/rest/tests/response_binary_test.cc create mode 100644 app/rest/tests/response_json_test.cc create mode 100644 app/rest/tests/response_test.cc create mode 100644 app/rest/tests/testdata/sample.fbs create mode 100644 app/rest/tests/transport_curl_test.cc create mode 100644 app/rest/tests/transport_mock_test.cc create mode 100644 app/rest/tests/util_test.cc create mode 100644 app/rest/tests/www_form_url_encoded_test.cc create mode 100644 app/rest/tests/zlibwrapper_unittest.cc create mode 100644 app/src/fake/FIRApp.h create mode 100644 app/src/fake/FIRApp.mm create mode 100644 app/src/fake/FIRConfiguration.h create mode 100644 app/src/fake/FIRConfiguration.m create mode 100644 app/src/fake/FIRLogger.h create mode 100644 app/src/fake/FIRLoggerLevel.h create mode 100644 app/src/fake/FIROptions.h create mode 100644 app/src/fake/FIROptions.mm create mode 100644 app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java create mode 100644 app/src_java/fake/com/google/android/gms/tasks/Task.java create mode 100644 app/src_java/fake/com/google/firebase/FirebaseApp.java create mode 100644 app/src_java/fake/com/google/firebase/FirebaseException.java create mode 100644 app/src_java/fake/com/google/firebase/FirebaseOptions.java create mode 100644 app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java create mode 100644 app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java create mode 100644 app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java create mode 100644 app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java create mode 100644 app/tests/CMakeLists.txt create mode 100644 app/tests/app_test.cc create mode 100644 app/tests/assert_test.cc create mode 100644 app/tests/base64_openssh_test.cc create mode 100644 app/tests/base64_test.cc create mode 100644 app/tests/callback_test.cc create mode 100644 app/tests/cleanup_notifier_test.cc create mode 100644 app/tests/flexbuffer_matcher.cc create mode 100644 app/tests/flexbuffer_matcher.h create mode 100644 app/tests/flexbuffer_matcher_test.cc create mode 100644 app/tests/future_manager_test.cc create mode 100644 app/tests/future_test.cc create mode 100644 app/tests/google_play_services/availability_android_test.cc create mode 100644 app/tests/google_services_test.cc create mode 100644 app/tests/include/firebase/app_for_testing.h create mode 100644 app/tests/intrusive_list_test.cc create mode 100644 app/tests/jobject_reference_test.cc create mode 100644 app/tests/locale_test.cc create mode 100644 app/tests/log_test.cc create mode 100644 app/tests/logger_test.cc create mode 100644 app/tests/optional_test.cc create mode 100644 app/tests/path_test.cc create mode 100644 app/tests/reference_count_test.cc create mode 100644 app/tests/scheduler_test.cc create mode 100644 app/tests/secure/user_secure_integration_test.cc create mode 100644 app/tests/secure/user_secure_internal_test.cc create mode 100644 app/tests/secure/user_secure_manager_test.cc create mode 100644 app/tests/semaphore_test.cc create mode 100644 app/tests/swizzle_test.mm create mode 100644 app/tests/thread_test.cc create mode 100644 app/tests/time_test.cc create mode 100644 app/tests/util_android_test.cc create mode 100644 app/tests/util_ios_test.mm create mode 100644 app/tests/uuid_test.cc create mode 100644 app/tests/variant_test.cc create mode 100644 app/tests/variant_util_test.cc create mode 100644 auth/src/ios/fake/FIRActionCodeSettings.h create mode 100644 auth/src/ios/fake/FIRAdditionalUserInfo.h create mode 100644 auth/src/ios/fake/FIRAdditionalUserInfo.mm create mode 100644 auth/src/ios/fake/FIRAuth.h create mode 100644 auth/src/ios/fake/FIRAuth.mm create mode 100644 auth/src/ios/fake/FIRAuthAPNSTokenType.h create mode 100644 auth/src/ios/fake/FIRAuthCredential.h create mode 100644 auth/src/ios/fake/FIRAuthCredential.mm create mode 100644 auth/src/ios/fake/FIRAuthDataResult.h create mode 100644 auth/src/ios/fake/FIRAuthDataResult.mm create mode 100644 auth/src/ios/fake/FIRAuthErrors.h create mode 100644 auth/src/ios/fake/FIRAuthSettings.h create mode 100644 auth/src/ios/fake/FIRAuthTokenResult.h create mode 100644 auth/src/ios/fake/FIRAuthUIDelegate.h create mode 100644 auth/src/ios/fake/FIREmailAuthProvider.h create mode 100644 auth/src/ios/fake/FIREmailAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRFacebookAuthProvider.h create mode 100644 auth/src/ios/fake/FIRFacebookAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRFederatedAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGameCenterAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGameCenterAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRGitHubAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGitHubAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRGoogleAuthProvider.h create mode 100644 auth/src/ios/fake/FIRGoogleAuthProvider.mm create mode 100644 auth/src/ios/fake/FIROAuthCredential.h create mode 100644 auth/src/ios/fake/FIROAuthCredential.mm create mode 100644 auth/src/ios/fake/FIROAuthProvider.h create mode 100644 auth/src/ios/fake/FIROAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRPhoneAuthCredential.h create mode 100644 auth/src/ios/fake/FIRPhoneAuthCredential.mm create mode 100644 auth/src/ios/fake/FIRPhoneAuthProvider.h create mode 100644 auth/src/ios/fake/FIRPhoneAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRTwitterAuthProvider.h create mode 100644 auth/src/ios/fake/FIRTwitterAuthProvider.mm create mode 100644 auth/src/ios/fake/FIRUser.h create mode 100644 auth/src/ios/fake/FIRUser.mm create mode 100644 auth/src/ios/fake/FIRUserInfo.h create mode 100644 auth/src/ios/fake/FIRUserMetadata.h create mode 100644 auth/src/ios/fake/FIRUserMetadata.mm create mode 100644 auth/src/ios/fake/FirebaseAuth.h create mode 100644 auth/src/ios/fake/FirebaseAuthVersion.h create mode 100644 auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java create mode 100644 auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java create mode 100644 auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/AuthCredential.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/AuthResult.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/UserInfo.java create mode 100644 auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java create mode 100644 auth/tests/CMakeLists.txt create mode 100644 auth/tests/auth_test.cc create mode 100644 auth/tests/credential_test.cc create mode 100644 auth/tests/desktop/auth_desktop_test.cc create mode 100644 auth/tests/desktop/fakes.cc create mode 100644 auth/tests/desktop/fakes.h create mode 100644 auth/tests/desktop/rpcs/create_auth_uri_test.cc create mode 100644 auth/tests/desktop/rpcs/delete_account_test.cc create mode 100644 auth/tests/desktop/rpcs/get_account_info_test.cc create mode 100644 auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc create mode 100644 auth/tests/desktop/rpcs/reset_password_test.cc create mode 100644 auth/tests/desktop/rpcs/secure_token_test.cc create mode 100644 auth/tests/desktop/rpcs/set_account_info_test.cc create mode 100644 auth/tests/desktop/rpcs/sign_up_new_user_test.cc create mode 100644 auth/tests/desktop/rpcs/test_util.cc create mode 100644 auth/tests/desktop/rpcs/test_util.h create mode 100644 auth/tests/desktop/rpcs/verify_assertion_test.cc create mode 100644 auth/tests/desktop/rpcs/verify_custom_token_test.cc create mode 100644 auth/tests/desktop/rpcs/verify_password_test.cc create mode 100644 auth/tests/desktop/test_utils.cc create mode 100644 auth/tests/desktop/test_utils.h create mode 100644 auth/tests/desktop/user_desktop_test.cc create mode 100644 auth/tests/user_test.cc create mode 100644 binary_to_array_test.py create mode 100644 build_type_header_test.py create mode 100644 database/src/ios/util_ios_test.mm create mode 100644 database/tests/CMakeLists.txt create mode 100644 database/tests/common/database_reference_test.cc create mode 100644 database/tests/desktop/connection/connection_test.cc create mode 100644 database/tests/desktop/connection/web_socket_client_impl_test.cc create mode 100644 database/tests/desktop/core/cache_policy_test.cc create mode 100644 database/tests/desktop/core/compound_write_test.cc create mode 100644 database/tests/desktop/core/event_registration_test.cc create mode 100644 database/tests/desktop/core/indexed_variant_test.cc create mode 100644 database/tests/desktop/core/operation_test.cc create mode 100644 database/tests/desktop/core/server_values_test.cc create mode 100644 database/tests/desktop/core/sparse_snapshot_tree_test.cc create mode 100644 database/tests/desktop/core/sync_point_test.cc create mode 100644 database/tests/desktop/core/sync_tree_test.cc create mode 100644 database/tests/desktop/core/tracked_query_manager_test.cc create mode 100644 database/tests/desktop/core/tree_test.cc create mode 100644 database/tests/desktop/core/write_tree_test.cc create mode 100644 database/tests/desktop/mutable_data_desktop_test.cc create mode 100644 database/tests/desktop/persistence/flatbuffer_conversions_test.cc create mode 100644 database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc create mode 100644 database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc create mode 100644 database/tests/desktop/persistence/noop_persistence_manager_test.cc create mode 100644 database/tests/desktop/persistence/persistence_manager_test.cc create mode 100644 database/tests/desktop/persistence/prune_forest_test.cc create mode 100644 database/tests/desktop/push_child_name_generator_test.cc create mode 100644 database/tests/desktop/test/matchers.h create mode 100644 database/tests/desktop/test/matchers_test.cc create mode 100644 database/tests/desktop/test/mock_cache_policy.h create mode 100644 database/tests/desktop/test/mock_listen_provider.h create mode 100644 database/tests/desktop/test/mock_listener.h create mode 100644 database/tests/desktop/test/mock_persistence_manager.h create mode 100644 database/tests/desktop/test/mock_persistence_storage_engine.h create mode 100644 database/tests/desktop/test/mock_tracked_query_manager.h create mode 100644 database/tests/desktop/test/mock_write_tree.h create mode 100644 database/tests/desktop/util_desktop_test.cc create mode 100644 database/tests/desktop/view/change_test.cc create mode 100644 database/tests/desktop/view/child_change_accumulator_test.cc create mode 100644 database/tests/desktop/view/event_generator_test.cc create mode 100644 database/tests/desktop/view/indexed_filter_test.cc create mode 100644 database/tests/desktop/view/limited_filter_test.cc create mode 100644 database/tests/desktop/view/ranged_filter_test.cc create mode 100644 database/tests/desktop/view/view_cache_test.cc create mode 100644 database/tests/desktop/view/view_processor_test.cc create mode 100644 database/tests/desktop/view/view_test.cc create mode 100755 firestore/generate_android_test.py create mode 100644 firestore/src/tests/android/field_path_portable_test.cc create mode 100644 firestore/src/tests/android/firebase_firestore_settings_android_test.cc create mode 100644 firestore/src/tests/android/geo_point_android_test.cc create mode 100644 firestore/src/tests/android/snapshot_metadata_android_test.cc create mode 100644 firestore/src/tests/android/timestamp_android_test.cc create mode 100644 firestore/src/tests/array_transform_test.cc create mode 100644 firestore/src/tests/cleanup_test.cc create mode 100644 firestore/src/tests/collection_reference_test.cc create mode 100644 firestore/src/tests/cursor_test.cc create mode 100644 firestore/src/tests/document_change_test.cc create mode 100644 firestore/src/tests/document_reference_test.cc create mode 100644 firestore/src/tests/document_snapshot_test.cc create mode 100644 firestore/src/tests/field_value_test.cc create mode 100644 firestore/src/tests/fields_test.cc create mode 100644 firestore/src/tests/firestore_integration_test.cc create mode 100644 firestore/src/tests/firestore_integration_test.h create mode 100644 firestore/src/tests/firestore_test.cc create mode 100644 firestore/src/tests/includes_test.cc create mode 100644 firestore/src/tests/listener_registration_test.cc create mode 100644 firestore/src/tests/numeric_transforms_test.cc create mode 100644 firestore/src/tests/query_network_test.cc create mode 100644 firestore/src/tests/query_snapshot_test.cc create mode 100644 firestore/src/tests/query_test.cc create mode 100644 firestore/src/tests/sanity_test.cc create mode 100644 firestore/src/tests/server_timestamp_test.cc create mode 100644 firestore/src/tests/smoke_test.cc create mode 100644 firestore/src/tests/transaction_extra_test.cc create mode 100644 firestore/src/tests/transaction_test.cc create mode 100644 firestore/src/tests/type_test.cc create mode 100644 firestore/src/tests/util/integration_test_util.cc create mode 100644 firestore/src/tests/util/integration_test_util_apple.mm create mode 100644 firestore/src/tests/validation_test.cc create mode 100644 firestore/src/tests/write_batch_test.cc create mode 100644 instance_id/src_ios/fake/FIRInstanceID.h create mode 100644 instance_id/src_ios/fake/FIRInstanceID.mm create mode 100644 instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java create mode 100644 instance_id/tests/CMakeLists.txt create mode 100644 instance_id/tests/instance_id_test.cc create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java create mode 100644 messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java create mode 100644 messaging/src/ios/fake/FIRMessaging.h create mode 100644 messaging/src/ios/fake/FIRMessaging.mm create mode 100644 messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java create mode 100644 messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java create mode 100644 messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java create mode 100644 messaging/tests/CMakeLists.txt create mode 100644 messaging/tests/android/cpp/message_reader_test.cc create mode 100644 messaging/tests/android/cpp/messaging_test_util.cc create mode 100644 messaging/tests/ios/messaging_test_util.mm create mode 100644 messaging/tests/messaging_test.cc create mode 100644 messaging/tests/messaging_test_util.h create mode 100644 remote_config/src/desktop/rest_fake.cc create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java create mode 100644 remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java create mode 100644 remote_config/tests/CMakeLists.txt create mode 100644 remote_config/tests/desktop/config_data_test.cc create mode 100644 remote_config/tests/desktop/file_manager_test.cc create mode 100644 remote_config/tests/desktop/metadata_test.cc create mode 100644 remote_config/tests/desktop/notification_channel_test.cc create mode 100644 remote_config/tests/desktop/remote_config_desktop_test.cc create mode 100644 remote_config/tests/desktop/rest_test.cc create mode 100644 remote_config/tests/remote_config_test.cc create mode 100644 storage/src/common/storage_uri_parser_test.cc create mode 100644 storage/tests/CMakeLists.txt create mode 100644 storage/tests/desktop/storage_desktop_utils_tests.cc create mode 100644 testing/config_test.cc create mode 100644 testing/reporter_impl_fake.cc create mode 100644 testing/reporter_impl_test.cc create mode 100644 testing/reporter_test.cc create mode 100644 testing/ticker_test.cc create mode 100644 testing/util_android_test.cc create mode 100644 testing/util_ios_test.mm create mode 100644 version_header_test.py diff --git a/admob/tools/ios/testapp/README.md b/admob/tools/ios/testapp/README.md new file mode 100644 index 0000000000..ad1c85a36d --- /dev/null +++ b/admob/tools/ios/testapp/README.md @@ -0,0 +1,57 @@ +Firebase AdMob iOS Test App +=========================== + +The Firebase AdMob iOS test app is designed to enable implementing, modifying, +and debugging API features directly in Xcode. + +Getting Started +--------------- + +- Get the code: + + git5 init + git5-track-p4-depot-paths //depot_firebase_cpp/admob/client/cpp/tools/ios/testapp/p4_depot_paths + git5 sync + +- Create the following symlinks (DO NOT check these in to google3 -- they should be added to your + .gitignore): + + NOTE: Firebase changed their includes from `include` to `src/include`. + These soft links work around the issue. + + GOOGLE3_PATH=~/path/to/git5/repo/google3 # Change to your google3 path + ln -s $GOOGLE3_PATH/firebase/app/client/cpp/src/include/ $GOOGLE3_PATH/firebase/app/client/cpp/include + ln -s $GOOGLE3_PATH/firebase/admob/client/cpp/src/include/ $GOOGLE3_PATH/firebase/admob/client/cpp/include + +Setting up the App +------------------ + +- In Project Navigator, add the GoogleMobileAds.framework to the Frameworks + testapp project. +- Update the following files: + - google3/firebase/admob/client/cpp/src/common/admob_common.cc + - Comment out the following code: + + /* + FIREBASE_APP_REGISTER_CALLBACKS(admob, + { + if (app == ::firebase::App::GetInstance()) { + return firebase::admob::Initialize(*app); + } + return kInitResultSuccess; + }, + { + if (app == ::firebase::App::GetInstance()) { + firebase::admob::Terminate(); + } + }); + */ + + - google3/firebase/admob/client/cpp/src/include/firebase/admob.h + - Comment out the following code: + + /* + #if !defined(DOXYGEN) && !defined(SWIG) + FIREBASE_APP_REGISTER_CALLBACKS_REFERENCE(admob) + #endif // !defined(DOXYGEN) && !defined(SWIG) + */ diff --git a/admob/tools/ios/testapp/p4_depot_paths b/admob/tools/ios/testapp/p4_depot_paths new file mode 100644 index 0000000000..96e3e23316 --- /dev/null +++ b/admob/tools/ios/testapp/p4_depot_paths @@ -0,0 +1,7 @@ +# Run the following command to git5 track the required directories for the +# Firebase-AdMob iOS test app: +# +# $ git5-track-p4-depot-paths //depot_firebase_cpp/admob/client/cpp/tools/ios/testapp/p4_depot_paths + +//depot_firebase_cpp/app/... +//depot_firebase_cpp/admob/... diff --git a/admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj b/admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a5be7f010d --- /dev/null +++ b/admob/tools/ios/testapp/testapp.xcodeproj/project.pbxproj @@ -0,0 +1,524 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 4AA541CC1CC6A9B400973957 /* GLKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AA541CB1CC6A9B400973957 /* GLKit.framework */; }; + 4AD13EA51CC9763C00AB0ACF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13E981CC9763C00AB0ACF /* AppDelegate.m */; }; + 4AD13EA61CC9763C00AB0ACF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AD13E991CC9763C00AB0ACF /* Assets.xcassets */; }; + 4AD13EA91CC9763C00AB0ACF /* game_engine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13E9F1CC9763C00AB0ACF /* game_engine.cpp */; }; + 4AD13EAB1CC9763C00AB0ACF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13EA21CC9763C00AB0ACF /* main.m */; }; + 4AD13EAC1CC9763C00AB0ACF /* ViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AD13EA41CC9763C00AB0ACF /* ViewController.mm */; }; + 4AD13EB11CC976C200AB0ACF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AD13EAD1CC976C200AB0ACF /* LaunchScreen.storyboard */; }; + 4AD13EB21CC976C200AB0ACF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AD13EAF1CC976C200AB0ACF /* Main.storyboard */; }; + 4AE90DEE1DBEC0AA00865A75 /* log_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DED1DBEC0AA00865A75 /* log_ios.mm */; }; + 4AE90DF61DBEC0DC00865A75 /* log.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DEF1DBEC0DC00865A75 /* log.cc */; }; + 4AE90DF71DBEC0DC00865A75 /* reference_counted_future_impl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DF21DBEC0DC00865A75 /* reference_counted_future_impl.cc */; }; + 4AE90DF81DBEC0DC00865A75 /* util_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DF51DBEC0DC00865A75 /* util_ios.mm */; }; + 4AE90E0C1DBEC0F300865A75 /* admob_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DF91DBEC0F300865A75 /* admob_ios.mm */; }; + 4AE90E0D1DBEC0F300865A75 /* banner_view_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DFB1DBEC0F300865A75 /* banner_view_internal_ios.mm */; }; + 4AE90E0E1DBEC0F300865A75 /* FADBannerView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DFD1DBEC0F300865A75 /* FADBannerView.mm */; }; + 4AE90E0F1DBEC0F300865A75 /* FADInterstitialDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90DFF1DBEC0F300865A75 /* FADInterstitialDelegate.mm */; }; + 4AE90E101DBEC0F300865A75 /* FADNativeExpressAdView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E011DBEC0F300865A75 /* FADNativeExpressAdView.mm */; }; + 4AE90E111DBEC0F300865A75 /* FADRequest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E031DBEC0F300865A75 /* FADRequest.mm */; }; + 4AE90E121DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E051DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm */; }; + 4AE90E131DBEC0F300865A75 /* interstitial_ad_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E071DBEC0F300865A75 /* interstitial_ad_internal_ios.mm */; }; + 4AE90E141DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E091DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm */; }; + 4AE90E151DBEC0F300865A75 /* rewarded_video_internal_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E0B1DBEC0F300865A75 /* rewarded_video_internal_ios.mm */; }; + 4AE90E241DBEC10700865A75 /* admob_common.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E161DBEC10700865A75 /* admob_common.cc */; }; + 4AE90E251DBEC10700865A75 /* banner_view_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E181DBEC10700865A75 /* banner_view_internal.cc */; }; + 4AE90E261DBEC10700865A75 /* banner_view.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1A1DBEC10700865A75 /* banner_view.cc */; }; + 4AE90E271DBEC10700865A75 /* interstitial_ad_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1B1DBEC10700865A75 /* interstitial_ad_internal.cc */; }; + 4AE90E281DBEC10700865A75 /* interstitial_ad.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1D1DBEC10700865A75 /* interstitial_ad.cc */; }; + 4AE90E291DBEC10700865A75 /* native_express_ad_view_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E1E1DBEC10700865A75 /* native_express_ad_view_internal.cc */; }; + 4AE90E2A1DBEC10700865A75 /* native_express_ad_view.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E201DBEC10700865A75 /* native_express_ad_view.cc */; }; + 4AE90E2B1DBEC10700865A75 /* rewarded_video_internal.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E211DBEC10700865A75 /* rewarded_video_internal.cc */; }; + 4AE90E2C1DBEC10700865A75 /* rewarded_video.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4AE90E231DBEC10700865A75 /* rewarded_video.cc */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 4AA541AF1CC6A3FE00973957 /* testapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = testapp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AA541CB1CC6A9B400973957 /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; }; + 4AD13E971CC9763C00AB0ACF /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = testapp/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + 4AD13E981CC9763C00AB0ACF /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = testapp/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + 4AD13E991CC9763C00AB0ACF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = testapp/Assets.xcassets; sourceTree = SOURCE_ROOT; }; + 4AD13E9F1CC9763C00AB0ACF /* game_engine.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = game_engine.cpp; path = testapp/game_engine.cpp; sourceTree = SOURCE_ROOT; }; + 4AD13EA01CC9763C00AB0ACF /* game_engine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = game_engine.h; path = testapp/game_engine.h; sourceTree = SOURCE_ROOT; }; + 4AD13EA11CC9763C00AB0ACF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = testapp/Info.plist; sourceTree = SOURCE_ROOT; }; + 4AD13EA21CC9763C00AB0ACF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = testapp/main.m; sourceTree = SOURCE_ROOT; }; + 4AD13EA31CC9763C00AB0ACF /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ViewController.h; path = testapp/ViewController.h; sourceTree = SOURCE_ROOT; }; + 4AD13EA41CC9763C00AB0ACF /* ViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ViewController.mm; path = testapp/ViewController.mm; sourceTree = SOURCE_ROOT; }; + 4AD13EAE1CC976C200AB0ACF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = testapp/Base.lproj/LaunchScreen.storyboard; sourceTree = SOURCE_ROOT; }; + 4AD13EB01CC976C200AB0ACF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = testapp/Base.lproj/Main.storyboard; sourceTree = SOURCE_ROOT; }; + 4AE90DED1DBEC0AA00865A75 /* log_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = log_ios.mm; path = ../../../../../../app/client/cpp/src/log_ios.mm; sourceTree = ""; }; + 4AE90DEF1DBEC0DC00865A75 /* log.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = log.cc; path = ../../../../../../app/client/cpp/src/log.cc; sourceTree = ""; }; + 4AE90DF01DBEC0DC00865A75 /* log.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = log.h; path = ../../../../../../app/client/cpp/src/log.h; sourceTree = ""; }; + 4AE90DF11DBEC0DC00865A75 /* mutex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = mutex.h; path = ../../../../../../app/client/cpp/src/mutex.h; sourceTree = ""; }; + 4AE90DF21DBEC0DC00865A75 /* reference_counted_future_impl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = reference_counted_future_impl.cc; path = ../../../../../../app/client/cpp/src/reference_counted_future_impl.cc; sourceTree = ""; }; + 4AE90DF31DBEC0DC00865A75 /* reference_counted_future_impl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = reference_counted_future_impl.h; path = ../../../../../../app/client/cpp/src/reference_counted_future_impl.h; sourceTree = ""; }; + 4AE90DF41DBEC0DC00865A75 /* util_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = util_ios.h; path = ../../../../../../app/client/cpp/src/util_ios.h; sourceTree = ""; }; + 4AE90DF51DBEC0DC00865A75 /* util_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = util_ios.mm; path = ../../../../../../app/client/cpp/src/util_ios.mm; sourceTree = ""; }; + 4AE90DF91DBEC0F300865A75 /* admob_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = admob_ios.mm; path = ../../../src/ios/admob_ios.mm; sourceTree = ""; }; + 4AE90DFA1DBEC0F300865A75 /* banner_view_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = banner_view_internal_ios.h; path = ../../../src/ios/banner_view_internal_ios.h; sourceTree = ""; }; + 4AE90DFB1DBEC0F300865A75 /* banner_view_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = banner_view_internal_ios.mm; path = ../../../src/ios/banner_view_internal_ios.mm; sourceTree = ""; }; + 4AE90DFC1DBEC0F300865A75 /* FADBannerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADBannerView.h; path = ../../../src/ios/FADBannerView.h; sourceTree = ""; }; + 4AE90DFD1DBEC0F300865A75 /* FADBannerView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADBannerView.mm; path = ../../../src/ios/FADBannerView.mm; sourceTree = ""; }; + 4AE90DFE1DBEC0F300865A75 /* FADInterstitialDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADInterstitialDelegate.h; path = ../../../src/ios/FADInterstitialDelegate.h; sourceTree = ""; }; + 4AE90DFF1DBEC0F300865A75 /* FADInterstitialDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADInterstitialDelegate.mm; path = ../../../src/ios/FADInterstitialDelegate.mm; sourceTree = ""; }; + 4AE90E001DBEC0F300865A75 /* FADNativeExpressAdView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADNativeExpressAdView.h; path = ../../../src/ios/FADNativeExpressAdView.h; sourceTree = ""; }; + 4AE90E011DBEC0F300865A75 /* FADNativeExpressAdView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADNativeExpressAdView.mm; path = ../../../src/ios/FADNativeExpressAdView.mm; sourceTree = ""; }; + 4AE90E021DBEC0F300865A75 /* FADRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADRequest.h; path = ../../../src/ios/FADRequest.h; sourceTree = ""; }; + 4AE90E031DBEC0F300865A75 /* FADRequest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADRequest.mm; path = ../../../src/ios/FADRequest.mm; sourceTree = ""; }; + 4AE90E041DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FADRewardBasedVideoAdDelegate.h; path = ../../../src/ios/FADRewardBasedVideoAdDelegate.h; sourceTree = ""; }; + 4AE90E051DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FADRewardBasedVideoAdDelegate.mm; path = ../../../src/ios/FADRewardBasedVideoAdDelegate.mm; sourceTree = ""; }; + 4AE90E061DBEC0F300865A75 /* interstitial_ad_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = interstitial_ad_internal_ios.h; path = ../../../src/ios/interstitial_ad_internal_ios.h; sourceTree = ""; }; + 4AE90E071DBEC0F300865A75 /* interstitial_ad_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = interstitial_ad_internal_ios.mm; path = ../../../src/ios/interstitial_ad_internal_ios.mm; sourceTree = ""; }; + 4AE90E081DBEC0F300865A75 /* native_express_ad_view_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = native_express_ad_view_internal_ios.h; path = ../../../src/ios/native_express_ad_view_internal_ios.h; sourceTree = ""; }; + 4AE90E091DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = native_express_ad_view_internal_ios.mm; path = ../../../src/ios/native_express_ad_view_internal_ios.mm; sourceTree = ""; }; + 4AE90E0A1DBEC0F300865A75 /* rewarded_video_internal_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rewarded_video_internal_ios.h; path = ../../../src/ios/rewarded_video_internal_ios.h; sourceTree = ""; }; + 4AE90E0B1DBEC0F300865A75 /* rewarded_video_internal_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = rewarded_video_internal_ios.mm; path = ../../../src/ios/rewarded_video_internal_ios.mm; sourceTree = ""; }; + 4AE90E161DBEC10700865A75 /* admob_common.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = admob_common.cc; path = ../../../src/common/admob_common.cc; sourceTree = ""; }; + 4AE90E171DBEC10700865A75 /* admob_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = admob_common.h; path = ../../../src/common/admob_common.h; sourceTree = ""; }; + 4AE90E181DBEC10700865A75 /* banner_view_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = banner_view_internal.cc; path = ../../../src/common/banner_view_internal.cc; sourceTree = ""; }; + 4AE90E191DBEC10700865A75 /* banner_view_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = banner_view_internal.h; path = ../../../src/common/banner_view_internal.h; sourceTree = ""; }; + 4AE90E1A1DBEC10700865A75 /* banner_view.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = banner_view.cc; path = ../../../src/common/banner_view.cc; sourceTree = ""; }; + 4AE90E1B1DBEC10700865A75 /* interstitial_ad_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = interstitial_ad_internal.cc; path = ../../../src/common/interstitial_ad_internal.cc; sourceTree = ""; }; + 4AE90E1C1DBEC10700865A75 /* interstitial_ad_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = interstitial_ad_internal.h; path = ../../../src/common/interstitial_ad_internal.h; sourceTree = ""; }; + 4AE90E1D1DBEC10700865A75 /* interstitial_ad.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = interstitial_ad.cc; path = ../../../src/common/interstitial_ad.cc; sourceTree = ""; }; + 4AE90E1E1DBEC10700865A75 /* native_express_ad_view_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = native_express_ad_view_internal.cc; path = ../../../src/common/native_express_ad_view_internal.cc; sourceTree = ""; }; + 4AE90E1F1DBEC10700865A75 /* native_express_ad_view_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = native_express_ad_view_internal.h; path = ../../../src/common/native_express_ad_view_internal.h; sourceTree = ""; }; + 4AE90E201DBEC10700865A75 /* native_express_ad_view.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = native_express_ad_view.cc; path = ../../../src/common/native_express_ad_view.cc; sourceTree = ""; }; + 4AE90E211DBEC10700865A75 /* rewarded_video_internal.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = rewarded_video_internal.cc; path = ../../../src/common/rewarded_video_internal.cc; sourceTree = ""; }; + 4AE90E221DBEC10700865A75 /* rewarded_video_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rewarded_video_internal.h; path = ../../../src/common/rewarded_video_internal.h; sourceTree = ""; }; + 4AE90E231DBEC10700865A75 /* rewarded_video.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = rewarded_video.cc; path = ../../../src/common/rewarded_video.cc; sourceTree = ""; }; + 4AE90E2D1DBEC12000865A75 /* banner_view_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = banner_view_internal_stub.h; path = ../../../src/stub/banner_view_internal_stub.h; sourceTree = ""; }; + 4AE90E2E1DBEC12000865A75 /* interstitial_ad_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = interstitial_ad_internal_stub.h; path = ../../../src/stub/interstitial_ad_internal_stub.h; sourceTree = ""; }; + 4AE90E2F1DBEC12000865A75 /* native_express_ad_view_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = native_express_ad_view_internal_stub.h; path = ../../../src/stub/native_express_ad_view_internal_stub.h; sourceTree = ""; }; + 4AE90E301DBEC12000865A75 /* rewarded_video_internal_stub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rewarded_video_internal_stub.h; path = ../../../src/stub/rewarded_video_internal_stub.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4AA541AC1CC6A3FE00973957 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AA541CC1CC6A9B400973957 /* GLKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4A1DEF141D0B27FC0002D14A /* common */ = { + isa = PBXGroup; + children = ( + 4AE90E161DBEC10700865A75 /* admob_common.cc */, + 4AE90E171DBEC10700865A75 /* admob_common.h */, + 4AE90E181DBEC10700865A75 /* banner_view_internal.cc */, + 4AE90E191DBEC10700865A75 /* banner_view_internal.h */, + 4AE90E1A1DBEC10700865A75 /* banner_view.cc */, + 4AE90E1B1DBEC10700865A75 /* interstitial_ad_internal.cc */, + 4AE90E1C1DBEC10700865A75 /* interstitial_ad_internal.h */, + 4AE90E1D1DBEC10700865A75 /* interstitial_ad.cc */, + 4AE90E1E1DBEC10700865A75 /* native_express_ad_view_internal.cc */, + 4AE90E1F1DBEC10700865A75 /* native_express_ad_view_internal.h */, + 4AE90E201DBEC10700865A75 /* native_express_ad_view.cc */, + 4AE90E211DBEC10700865A75 /* rewarded_video_internal.cc */, + 4AE90E221DBEC10700865A75 /* rewarded_video_internal.h */, + 4AE90E231DBEC10700865A75 /* rewarded_video.cc */, + ); + name = common; + sourceTree = ""; + }; + 4A1DEF151D0B28030002D14A /* ios */ = { + isa = PBXGroup; + children = ( + 4AE90DF91DBEC0F300865A75 /* admob_ios.mm */, + 4AE90DFA1DBEC0F300865A75 /* banner_view_internal_ios.h */, + 4AE90DFB1DBEC0F300865A75 /* banner_view_internal_ios.mm */, + 4AE90DFC1DBEC0F300865A75 /* FADBannerView.h */, + 4AE90DFD1DBEC0F300865A75 /* FADBannerView.mm */, + 4AE90DFE1DBEC0F300865A75 /* FADInterstitialDelegate.h */, + 4AE90DFF1DBEC0F300865A75 /* FADInterstitialDelegate.mm */, + 4AE90E001DBEC0F300865A75 /* FADNativeExpressAdView.h */, + 4AE90E011DBEC0F300865A75 /* FADNativeExpressAdView.mm */, + 4AE90E021DBEC0F300865A75 /* FADRequest.h */, + 4AE90E031DBEC0F300865A75 /* FADRequest.mm */, + 4AE90E041DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.h */, + 4AE90E051DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm */, + 4AE90E061DBEC0F300865A75 /* interstitial_ad_internal_ios.h */, + 4AE90E071DBEC0F300865A75 /* interstitial_ad_internal_ios.mm */, + 4AE90E081DBEC0F300865A75 /* native_express_ad_view_internal_ios.h */, + 4AE90E091DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm */, + 4AE90E0A1DBEC0F300865A75 /* rewarded_video_internal_ios.h */, + 4AE90E0B1DBEC0F300865A75 /* rewarded_video_internal_ios.mm */, + ); + name = ios; + sourceTree = ""; + }; + 4A242D501D45D5B500A98845 /* stub */ = { + isa = PBXGroup; + children = ( + 4AE90E2D1DBEC12000865A75 /* banner_view_internal_stub.h */, + 4AE90E2E1DBEC12000865A75 /* interstitial_ad_internal_stub.h */, + 4AE90E2F1DBEC12000865A75 /* native_express_ad_view_internal_stub.h */, + 4AE90E301DBEC12000865A75 /* rewarded_video_internal_stub.h */, + ); + name = stub; + sourceTree = ""; + }; + 4AA541A61CC6A3FE00973957 = { + isa = PBXGroup; + children = ( + 4AA542A41CC822BC00973957 /* firebase */, + 4AA5427F1CC6B70F00973957 /* firebase_admob */, + 4AA541B11CC6A3FE00973957 /* testapp */, + 4AA541C91CC6A5F500973957 /* Frameworks */, + 4AA541B01CC6A3FE00973957 /* Products */, + ); + sourceTree = ""; + }; + 4AA541B01CC6A3FE00973957 /* Products */ = { + isa = PBXGroup; + children = ( + 4AA541AF1CC6A3FE00973957 /* testapp.app */, + ); + name = Products; + sourceTree = ""; + }; + 4AA541B11CC6A3FE00973957 /* testapp */ = { + isa = PBXGroup; + children = ( + 4AD13E971CC9763C00AB0ACF /* AppDelegate.h */, + 4AD13E981CC9763C00AB0ACF /* AppDelegate.m */, + 4AD13EA31CC9763C00AB0ACF /* ViewController.h */, + 4AD13EA41CC9763C00AB0ACF /* ViewController.mm */, + 4AD13EA01CC9763C00AB0ACF /* game_engine.h */, + 4AD13E9F1CC9763C00AB0ACF /* game_engine.cpp */, + 4AD13E991CC9763C00AB0ACF /* Assets.xcassets */, + 4AD13EAD1CC976C200AB0ACF /* LaunchScreen.storyboard */, + 4AD13EAF1CC976C200AB0ACF /* Main.storyboard */, + 4AD13EA11CC9763C00AB0ACF /* Info.plist */, + 4AA541B21CC6A3FE00973957 /* Supporting Files */, + ); + path = testapp; + sourceTree = ""; + }; + 4AA541B21CC6A3FE00973957 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4AD13EA21CC9763C00AB0ACF /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 4AA541C91CC6A5F500973957 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4AA541CB1CC6A9B400973957 /* GLKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4AA5427F1CC6B70F00973957 /* firebase_admob */ = { + isa = PBXGroup; + children = ( + 4A1DEF151D0B28030002D14A /* ios */, + 4A1DEF141D0B27FC0002D14A /* common */, + 4A242D501D45D5B500A98845 /* stub */, + ); + name = firebase_admob; + sourceTree = ""; + }; + 4AA542A41CC822BC00973957 /* firebase */ = { + isa = PBXGroup; + children = ( + 4AE90DEF1DBEC0DC00865A75 /* log.cc */, + 4AE90DF01DBEC0DC00865A75 /* log.h */, + 4AE90DF11DBEC0DC00865A75 /* mutex.h */, + 4AE90DF21DBEC0DC00865A75 /* reference_counted_future_impl.cc */, + 4AE90DF31DBEC0DC00865A75 /* reference_counted_future_impl.h */, + 4AE90DF41DBEC0DC00865A75 /* util_ios.h */, + 4AE90DF51DBEC0DC00865A75 /* util_ios.mm */, + 4AE90DED1DBEC0AA00865A75 /* log_ios.mm */, + ); + name = firebase; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4AA541AE1CC6A3FE00973957 /* testapp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AA541C61CC6A3FE00973957 /* Build configuration list for PBXNativeTarget "testapp" */; + buildPhases = ( + 4AA541AB1CC6A3FE00973957 /* Sources */, + 4AA541AC1CC6A3FE00973957 /* Frameworks */, + 4AA541AD1CC6A3FE00973957 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = testapp; + productName = testapp; + productReference = 4AA541AF1CC6A3FE00973957 /* testapp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4AA541A71CC6A3FE00973957 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 4AA541AE1CC6A3FE00973957 = { + CreatedOnToolsVersion = 7.3; + }; + }; + }; + buildConfigurationList = 4AA541AA1CC6A3FE00973957 /* Build configuration list for PBXProject "testapp" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4AA541A61CC6A3FE00973957; + productRefGroup = 4AA541B01CC6A3FE00973957 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4AA541AE1CC6A3FE00973957 /* testapp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4AA541AD1CC6A3FE00973957 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AD13EB11CC976C200AB0ACF /* LaunchScreen.storyboard in Resources */, + 4AD13EA61CC9763C00AB0ACF /* Assets.xcassets in Resources */, + 4AD13EB21CC976C200AB0ACF /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4AA541AB1CC6A3FE00973957 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AE90DF71DBEC0DC00865A75 /* reference_counted_future_impl.cc in Sources */, + 4AE90E291DBEC10700865A75 /* native_express_ad_view_internal.cc in Sources */, + 4AE90DF61DBEC0DC00865A75 /* log.cc in Sources */, + 4AE90E2A1DBEC10700865A75 /* native_express_ad_view.cc in Sources */, + 4AE90E281DBEC10700865A75 /* interstitial_ad.cc in Sources */, + 4AE90E151DBEC0F300865A75 /* rewarded_video_internal_ios.mm in Sources */, + 4AE90DEE1DBEC0AA00865A75 /* log_ios.mm in Sources */, + 4AE90DF81DBEC0DC00865A75 /* util_ios.mm in Sources */, + 4AD13EA51CC9763C00AB0ACF /* AppDelegate.m in Sources */, + 4AE90E2C1DBEC10700865A75 /* rewarded_video.cc in Sources */, + 4AE90E261DBEC10700865A75 /* banner_view.cc in Sources */, + 4AE90E241DBEC10700865A75 /* admob_common.cc in Sources */, + 4AE90E271DBEC10700865A75 /* interstitial_ad_internal.cc in Sources */, + 4AE90E0D1DBEC0F300865A75 /* banner_view_internal_ios.mm in Sources */, + 4AD13EAC1CC9763C00AB0ACF /* ViewController.mm in Sources */, + 4AE90E0E1DBEC0F300865A75 /* FADBannerView.mm in Sources */, + 4AE90E2B1DBEC10700865A75 /* rewarded_video_internal.cc in Sources */, + 4AD13EA91CC9763C00AB0ACF /* game_engine.cpp in Sources */, + 4AE90E111DBEC0F300865A75 /* FADRequest.mm in Sources */, + 4AE90E0F1DBEC0F300865A75 /* FADInterstitialDelegate.mm in Sources */, + 4AE90E131DBEC0F300865A75 /* interstitial_ad_internal_ios.mm in Sources */, + 4AE90E121DBEC0F300865A75 /* FADRewardBasedVideoAdDelegate.mm in Sources */, + 4AE90E251DBEC10700865A75 /* banner_view_internal.cc in Sources */, + 4AE90E0C1DBEC0F300865A75 /* admob_ios.mm in Sources */, + 4AE90E101DBEC0F300865A75 /* FADNativeExpressAdView.mm in Sources */, + 4AD13EAB1CC9763C00AB0ACF /* main.m in Sources */, + 4AE90E141DBEC0F300865A75 /* native_express_ad_view_internal_ios.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 4AD13EAD1CC976C200AB0ACF /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4AD13EAE1CC976C200AB0ACF /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 4AD13EAF1CC976C200AB0ACF /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4AD13EB01CC976C200AB0ACF /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4AA541C41CC6A3FE00973957 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + "-Wswitch-enum", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(SRCROOT)//../../../../../../../ $(SRCROOT)/../../../../../../../firebase/admob/client/cpp/src $(SRCROOT)//../../../../../../../firebase/admob/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include/firebase"; + }; + name = Debug; + }; + 4AA541C51CC6A3FE00973957 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-Wswitch-enum", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(SRCROOT)//../../../../../../../ $(SRCROOT)/../../../../../../../firebase/admob/client/cpp/src $(SRCROOT)//../../../../../../../firebase/admob/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include $(SRCROOT)/../../../../../../../firebase/app/client/cpp/src/include/firebase"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4AA541C71CC6A3FE00973957 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImages; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = testapp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.ios.admob.testapp; + PRODUCT_NAME = testapp; + }; + name = Debug; + }; + 4AA541C81CC6A3FE00973957 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImages; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = testapp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.ios.admob.testapp; + PRODUCT_NAME = testapp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4AA541AA1CC6A3FE00973957 /* Build configuration list for PBXProject "testapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AA541C41CC6A3FE00973957 /* Debug */, + 4AA541C51CC6A3FE00973957 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AA541C61CC6A3FE00973957 /* Build configuration list for PBXNativeTarget "testapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AA541C71CC6A3FE00973957 /* Debug */, + 4AA541C81CC6A3FE00973957 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4AA541A71CC6A3FE00973957 /* Project object */; +} diff --git a/admob/tools/ios/testapp/testapp/AppDelegate.h b/admob/tools/ios/testapp/testapp/AppDelegate.h new file mode 100644 index 0000000000..2701a9510d --- /dev/null +++ b/admob/tools/ios/testapp/testapp/AppDelegate.h @@ -0,0 +1,11 @@ +// Copyright © 2016 Google. All rights reserved. + +@import GoogleMobileAds; + +#import + +@interface AppDelegate : UIResponder + +@property(nonatomic, strong) UIWindow *window; + +@end diff --git a/admob/tools/ios/testapp/testapp/AppDelegate.m b/admob/tools/ios/testapp/testapp/AppDelegate.m new file mode 100644 index 0000000000..1c561de93d --- /dev/null +++ b/admob/tools/ios/testapp/testapp/AppDelegate.m @@ -0,0 +1,16 @@ +// Copyright © 2016 Google. All rights reserved. + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + return YES; +} + +@end diff --git a/admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json b/admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..eeea76c2db --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json b/admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json b/admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json new file mode 100644 index 0000000000..a0ad363c85 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Assets.xcassets/LaunchImages.launchimage/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard b/admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..94b4751591 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard b/admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..98dfe0f664 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admob/tools/ios/testapp/testapp/Info.plist b/admob/tools/ios/testapp/testapp/Info.plist new file mode 100644 index 0000000000..1bcde66117 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/Info.plist @@ -0,0 +1,50 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/admob/tools/ios/testapp/testapp/ViewController.h b/admob/tools/ios/testapp/testapp/ViewController.h new file mode 100644 index 0000000000..5c0b4cd70f --- /dev/null +++ b/admob/tools/ios/testapp/testapp/ViewController.h @@ -0,0 +1,8 @@ +// Copyright © 2016 Google. All rights reserved. + +#import +#import + +@interface ViewController : UIViewController + +@end diff --git a/admob/tools/ios/testapp/testapp/ViewController.mm b/admob/tools/ios/testapp/testapp/ViewController.mm new file mode 100644 index 0000000000..fe9446ac0b --- /dev/null +++ b/admob/tools/ios/testapp/testapp/ViewController.mm @@ -0,0 +1,135 @@ +// Copyright © 2016 Google. All rights reserved. + +#import "admob/tools/ios/testapp/testapp/ViewController.h" + +#import "admob/tools/ios/testapp/testapp/game_engine.h" + +@interface ViewController () { + /// The AdMob C++ Wrapper Game Engine. + GameEngine *_gameEngine; + + /// The GLKView provides a default implementation of an OpenGL ES view. + GLKView *_glkView; +} + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Set up the GLKView. + _glkView = [[GLKView alloc] init]; + [self setUpLayoutConstraintsForGLKView]; + [self.view addSubview:_glkView]; + + [self setUpGL]; +} + +#pragma mark - GLKView Setup Methods + +- (void)setUpGL { + // Set the OpenGL ES rendering context. + _glkView.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + [EAGLContext setCurrentContext:_glkView.context]; + _glkView.drawableDepthFormat = GLKViewDrawableDepthFormat24; + _glkView.delegate = self; + + // Allocate and initialize a GLKViewController to implement an OpenGL ES rendering loop. + GLKViewController *viewController = [[GLKViewController alloc] initWithNibName:nil bundle:nil]; + viewController.view = _glkView; + viewController.delegate = self; + [self addChildViewController:viewController]; + + // Create a C++ GameEngine object and call the set up methods. + _gameEngine = new GameEngine(); + self->_gameEngine->Initialize(self.view); + self->_gameEngine->onSurfaceCreated(); + // Making the assumption that the glkView is equal to the mainScreen size. In other words, the + // glkView is full screen. + CGSize screenSize = [[[UIScreen mainScreen] currentMode] size]; + self->_gameEngine->onSurfaceChanged(screenSize.width, screenSize.height); + + // Set up the UITapGestureRecognizer for the GLKView. + UITapGestureRecognizer *tapRecognizer = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; + tapRecognizer.numberOfTapsRequired = 1; + [self.view addGestureRecognizer:tapRecognizer]; +} + +- (void)setUpLayoutConstraintsForGLKView { + [self.view addSubview:_glkView]; + _glkView.translatesAutoresizingMaskIntoConstraints = NO; + + // Layout constraints that match the parent view. + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeHeight + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:_glkView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeWidth + multiplier:1 + constant:0]]; +} + +#pragma mark - GLKViewDelegate + +- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { + self->_gameEngine->onDrawFrame(); +} + +#pragma mark - GLKViewController + +- (void)glkViewControllerUpdate:(GLKViewController *)controller { + self->_gameEngine->onUpdate(); +} + +#pragma mark - Actions + +- (void)handleTap:(UITapGestureRecognizer *)recognizer { + if (recognizer.state == UIGestureRecognizerStateEnded) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + CGFloat scale = [[UIScreen mainScreen] scale]; + CGPoint tapLocation = [recognizer locationInView:self->_glkView]; + // Map the x and y coordinates to pixel values using the scale factor associated with the + // device's screen. + int scaledX = tapLocation.x * scale; + int scaledY = tapLocation.y * scale; + self->_gameEngine->onTap(scaledX, scaledY); + }); + } +} + +#pragma mark - Log Message + +// Log a message that can be viewed in the console. +int LogMessage(const char* format, ...) { + va_list list; + int rc = 0; + va_start(list, format); + NSLogv(@(format), list); + va_end(list); + return rc; +} + +@end diff --git a/admob/tools/ios/testapp/testapp/game_engine.cpp b/admob/tools/ios/testapp/testapp/game_engine.cpp new file mode 100644 index 0000000000..270f90c688 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/game_engine.cpp @@ -0,0 +1,551 @@ +// Copyright © 2016 Google. All rights reserved. + +#include "admob/tools/ios/testapp/testapp/game_engine.h" + +#include + +#include "app/src/assert.h" + +namespace rewarded_video = firebase::admob::rewarded_video; + +// AdMob app ID. +const char* kAdMobAppID = "ca-app-pub-3940256099942544~1458002511"; + +// AdMob ad unit IDs. +const char* kBannerAdUnit = "ca-app-pub-3940256099942544/2934735716"; +const char* kNativeExpressAdUnit = "ca-app-pub-3940256099942544/2562852117"; +const char* kInterstitialAdUnit = "ca-app-pub-3940256099942544/4411468910"; +const char* kRewardedVideoAdUnit = "ca-app-pub-2618531387707574/6671583249"; + +// A simple listener that logs changes to a BannerView. +class LoggingBannerViewListener : public firebase::admob::BannerView::Listener { + public: + LoggingBannerViewListener() {} + void OnPresentationStateChanged( + firebase::admob::BannerView* banner_view, + firebase::admob::BannerView::PresentationState state) override { + LogMessage("BannerView PresentationState has changed to %d.", state); + } + void OnBoundingBoxChanged(firebase::admob::BannerView* banner_view, + firebase::admob::BoundingBox box) override { + LogMessage( + "BannerView BoundingBox has changed to (x: %d, y: %d, width: %d, " + "height %d)", + box.x, box.y, box.width, box.height); + } +}; + +// A simple listener that logs changes to a NativeExpressAdView. +class LoggingNativeExpressAdViewListener + : public firebase::admob::NativeExpressAdView::Listener { + public: + LoggingNativeExpressAdViewListener() {} + void OnPresentationStateChanged( + firebase::admob::NativeExpressAdView* native_express_view, + firebase::admob::NativeExpressAdView::PresentationState state) override { + LogMessage("NativeExpressAdView PresentationState has changed to %d.", + state); + } + void OnBoundingBoxChanged( + firebase::admob::NativeExpressAdView* native_express_view, + firebase::admob::BoundingBox box) override { + LogMessage( + "NativeExpressAd BoundingBox has changed to (x: %d, y: %d, width: %d, " + "height %d)", + box.x, box.y, box.width, box.height); + } +}; + +// A simple listener that logs changes to an InterstitialAd. +class LoggingInterstitialAdListener + : public firebase::admob::InterstitialAd::Listener { + public: + LoggingInterstitialAdListener() {} + void OnPresentationStateChanged( + firebase::admob::InterstitialAd* interstitial_ad, + firebase::admob::InterstitialAd::PresentationState state) override { + LogMessage("InterstitialAd PresentationState has changed to %d.", state); + } +}; + +// A simple listener that logs changes to rewarded video state. +class LoggingRewardedVideoListener : public rewarded_video::Listener { + public: + LoggingRewardedVideoListener() {} + void OnRewarded(rewarded_video::RewardItem reward) override { + LogMessage("Reward user with %f %s.", reward.amount, + reward.reward_type.c_str()); + } + void OnPresentationStateChanged( + rewarded_video::PresentationState state) override { + LogMessage("Rewarded video PresentationState has changed to %d.", state); + } +}; + +// The listeners for logging changes to the AdMob ad formats. +LoggingBannerViewListener banner_listener; +LoggingNativeExpressAdViewListener native_express_listener; +LoggingInterstitialAdListener interstitial_listener; +LoggingRewardedVideoListener rewarded_listener; + +// GameEngine constructor. +GameEngine::GameEngine() {} + +// Sets up AdMob C++. +void GameEngine::Initialize(firebase::admob::AdParent ad_parent) { + FIREBASE_ASSERT(kTestBannerView != kTestNativeExpressAdView && + "kTestBannerView and kTestNativeExpressAdView cannot both be " + "true/false at the same time."); + FIREBASE_ASSERT(kTestInterstitialAd != kTestRewardedVideo && + "kTestInterstitialAd and kTestRewardedVideo cannot both be " + "true/false at the same time."); + + firebase::admob::Initialize(kAdMobAppID); + parent_view_ = ad_parent; + + if (kTestBannerView) { + // Create an ad size and initialize the BannerView. + firebase::admob::AdSize bannerAdSize; + bannerAdSize.width = 320; + bannerAdSize.height = 50; + banner_view_ = new firebase::admob::BannerView(); + banner_view_->Initialize(parent_view_, kBannerAdUnit, bannerAdSize); + banner_view_listener_set_ = false; + } + + if (kTestNativeExpressAdView) { + // Create an ad size and initialize the NativeExpressAdView. + firebase::admob::AdSize nativeExpressAdSize; + nativeExpressAdSize.width = 320; + nativeExpressAdSize.height = 220; + native_express_view_ = new firebase::admob::NativeExpressAdView(); + native_express_view_->Initialize(parent_view_, kNativeExpressAdUnit, + nativeExpressAdSize); + native_express_ad_view_listener_set_ = false; + } + + if (kTestInterstitialAd) { + // Initialize the InterstitialAd. + interstitial_ad_ = new firebase::admob::InterstitialAd(); + interstitial_ad_->Initialize(parent_view_, kInterstitialAdUnit); + interstitial_ad_listener_set_ = false; + } + + if (kTestRewardedVideo) { + // Initialize the rewarded_video:: namespace. + rewarded_video::Initialize(); + // If you want to poll the reward, uncomment the poll_listener_ code in the + // update() function. When the poll_listener_code is commented out in + // update(), then the LoggingRewardedVideoListener is used to log changes to + // the rewarded video state. + poll_listener_ = nullptr; + rewarded_video_listener_set_ = false; + } +} + +// Creates the AdMob C++ ad request. +firebase::admob::AdRequest GameEngine::createRequest() { + // Sample keywords to use in making the request. + static const char* kKeywords[] = {"AdMob", "C++", "Fun"}; + + // Sample test device IDs to use in making the request. + static const char* kTestDeviceIDs[] = {"2077ef9a63d2b398840261c8221a0c9b", + "098fe087d987c9a878965454a65654d7"}; + + // Sample birthday value to use in making the request. + static const int kBirthdayDay = 10; + static const int kBirthdayMonth = 11; + static const int kBirthdayYear = 1976; + + firebase::admob::AdRequest request; + request.gender = firebase::admob::kGenderUnknown; + + request.tagged_for_child_directed_treatment = + firebase::admob::kChildDirectedTreatmentStateTagged; + + request.birthday_day = kBirthdayDay; + request.birthday_month = kBirthdayMonth; + request.birthday_year = kBirthdayYear; + + request.keyword_count = sizeof(kKeywords) / sizeof(kKeywords[0]); + request.keywords = kKeywords; + + static const firebase::admob::KeyValuePair kRequestExtras[] = { + {"the_name_of_an_extra", "the_value_for_that_extra"}}; + request.extras_count = sizeof(kRequestExtras) / sizeof(kRequestExtras[0]); + request.extras = kRequestExtras; + + request.test_device_id_count = + sizeof(kTestDeviceIDs) / sizeof(kTestDeviceIDs[0]); + request.test_device_ids = kTestDeviceIDs; + + return request; +} + +// Updates the game engine (game loop). +void GameEngine::onUpdate() { + if (kTestBannerView) { + // Set the banner view listener. + if (banner_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !banner_view_listener_set_) { + banner_view_->SetListener(&banner_listener); + banner_view_listener_set_ = true; + } + } + + if (kTestNativeExpressAdView) { + // Set the native express ad view listener. + if (native_express_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !native_express_ad_view_listener_set_) { + native_express_view_->SetListener(&native_express_listener); + native_express_ad_view_listener_set_ = true; + } + } + + if (kTestInterstitialAd) { + // Set the interstitial ad listener. + if (interstitial_ad_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !interstitial_ad_listener_set_) { + interstitial_ad_->SetListener(&interstitial_listener); + interstitial_ad_listener_set_ = true; + } + + // Once the interstitial ad has been displayed to and dismissed by the user, + // create a new interstitial ad. + if (interstitial_ad_->ShowLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->ShowLastResult().Error() == + firebase::admob::kAdMobErrorNone && + interstitial_ad_->GetPresentationState() == + firebase::admob::InterstitialAd::kPresentationStateHidden) { + delete interstitial_ad_; + interstitial_ad_ = nullptr; + interstitial_ad_ = new firebase::admob::InterstitialAd(); + interstitial_ad_->Initialize(parent_view_, kInterstitialAdUnit); + interstitial_ad_listener_set_ = false; + } + } + + if (kTestRewardedVideo) { + // Set the rewarded video listener. + if (rewarded_video::InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone && + !rewarded_video_listener_set_) { + // && poll_listener == nullptr) { + rewarded_video::SetListener(&rewarded_listener); + rewarded_video_listener_set_ = true; + // poll_listener_ = new + // firebase::admob::rewarded_video::PollableRewardListener(); + // rewarded_video::SetListener(poll_listener_); + } + + // Once the rewarded video ad has been displayed to and dismissed by the + // user, create a new rewarded video ad. + if (rewarded_video::ShowLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::ShowLastResult().Error() == + firebase::admob::kAdMobErrorNone && + rewarded_video::GetPresentationState() == + firebase::admob::rewarded_video::kPresentationStateHidden) { + rewarded_video::Destroy(); + rewarded_video::Initialize(); + rewarded_video_listener_set_ = false; + } + } + + // Increment red if increasing, decrement otherwise. + float diff = bg_intensity_increasing_ ? 0.0025f : -0.0025f; + + // Increment red up to 1.0, then back down to 0.0, repeat. + bg_intensity_ += diff; + if (bg_intensity_ >= 0.4f) { + bg_intensity_increasing_ = false; + } else if (bg_intensity_ <= 0.0f) { + bg_intensity_increasing_ = true; + } +} + +// Handles user tapping on one of the kNumberOfButtons. +void GameEngine::onTap(float x, float y) { + int button_number = -1; + GLfloat viewport_x = 1 - (((width_ - x) * 2) / width_); + GLfloat viewport_y = 1 - (((y)*2) / height_); + + for (int i = 0; i < kNumberOfButtons; i++) { + if ((viewport_x >= vertices_[i * 8]) && + (viewport_x <= vertices_[i * 8 + 2]) && + (viewport_y <= vertices_[i * 8 + 1]) && + (viewport_y >= vertices_[i * 8 + 5])) { + button_number = i; + break; + } + } + + // The BannerView or NativeExpressAdView's bounding box. + firebase::admob::BoundingBox box; + + switch (button_number) { + case 0: + if (kTestBannerView) { + // Load the banner ad. + if (banner_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + banner_view_->LoadAd(createRequest()); + } + } + if (kTestNativeExpressAdView) { + // Load the native express ad. + if (native_express_view_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + native_express_view_->LoadAd(createRequest()); + } + } + break; + case 1: + if (kTestBannerView) { + // Show/Hide the BannerView. + if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + banner_view_->GetPresentationState() == + firebase::admob::BannerView::kPresentationStateHidden) { + banner_view_->Show(); + } else if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->GetPresentationState() == + firebase::admob::BannerView:: + kPresentationStateVisibleWithAd) { + banner_view_->Hide(); + } + } + if (kTestNativeExpressAdView) { + // Show/Hide the NativeExpressAdView. + if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + native_express_view_->GetPresentationState() == + firebase::admob::NativeExpressAdView:: + kPresentationStateHidden) { + native_express_view_->Show(); + } else if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + native_express_view_->GetPresentationState() == + firebase::admob::NativeExpressAdView:: + kPresentationStateVisibleWithAd) { + native_express_view_->Hide(); + } + } + break; + case 2: + if (kTestBannerView) { + // Move the BannerView to a predefined position. + if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + banner_view_->MoveTo(firebase::admob::BannerView::kPositionBottom); + } + } + if (kTestNativeExpressAdView) { + // Move the NativeExpressAdView to a predefined position. + if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + native_express_view_->MoveTo( + firebase::admob::NativeExpressAdView::kPositionBottom); + } + } + break; + case 3: + if (kTestBannerView) { + // Move the BannerView to a specific x and y coordinate. + if (banner_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + banner_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + int x = 100; + int y = 200; + banner_view_->MoveTo(x, y); + } + } + if (kTestNativeExpressAdView) { + // Move the NativeExpressAdView to a specific x and y coordinate. + if (native_express_view_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + native_express_view_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + int x = 100; + int y = 200; + native_express_view_->MoveTo(x, y); + } + } + if (kTestRewardedVideo) { + // Poll the reward. + if (poll_listener_ != nullptr) { + while (poll_listener_->PollReward(&reward_)) { + LogMessage("Reward user with %f %s.", reward_.amount, + reward_.reward_type.c_str()); + } + } + } + break; + case 4: + if (kTestInterstitialAd) { + // Load the interstitial ad. + if (interstitial_ad_->InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + interstitial_ad_->LoadAd(createRequest()); + } + } + if (kTestRewardedVideo) { + // Load the rewarded video ad. + if (rewarded_video::InitializeLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::InitializeLastResult().Error() == + firebase::admob::kAdMobErrorNone) { + rewarded_video::LoadAd(kRewardedVideoAdUnit, createRequest()); + } + } + break; + case 5: + if (kTestInterstitialAd) { + // Show the interstitial ad. + if (interstitial_ad_->LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + interstitial_ad_->LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + interstitial_ad_->ShowLastResult().Status() != + firebase::kFutureStatusComplete) { + interstitial_ad_->Show(); + } + } + if (kTestRewardedVideo) { + // Show the rewarded video ad. + if (rewarded_video::LoadAdLastResult().Status() == + firebase::kFutureStatusComplete && + rewarded_video::LoadAdLastResult().Error() == + firebase::admob::kAdMobErrorNone && + rewarded_video::ShowLastResult().Status() != + firebase::kFutureStatusComplete) { + rewarded_video::Show(parent_view_); + } + } + break; + default: + break; + } +} + +// The vertex shader code string. +static const GLchar* kVertexShaderCodeString = + "attribute vec2 position;\n" + "\n" + "void main()\n" + "{\n" + " gl_Position = vec4(position, 0.0, 1.0);\n" + "}"; + +// The fragment shader code string. +static const GLchar* kFragmentShaderCodeString = + "precision mediump float;\n" + "uniform vec4 myColor; \n" + "void main() { \n" + " gl_FragColor = myColor; \n" + "}"; + +// Creates the OpenGL surface. +void GameEngine::onSurfaceCreated() { + vertex_shader_ = glCreateShader(GL_VERTEX_SHADER); + fragment_shader_ = glCreateShader(GL_FRAGMENT_SHADER); + + glShaderSource(vertex_shader_, 1, &kVertexShaderCodeString, NULL); + glCompileShader(vertex_shader_); + + GLint status; + glGetShaderiv(vertex_shader_, GL_COMPILE_STATUS, &status); + + char buffer[512]; + glGetShaderInfoLog(vertex_shader_, 512, NULL, buffer); + + glShaderSource(fragment_shader_, 1, &kFragmentShaderCodeString, NULL); + glCompileShader(fragment_shader_); + + glGetShaderiv(fragment_shader_, GL_COMPILE_STATUS, &status); + + glGetShaderInfoLog(fragment_shader_, 512, NULL, buffer); + + shader_program_ = glCreateProgram(); + glAttachShader(shader_program_, vertex_shader_); + glAttachShader(shader_program_, fragment_shader_); + + glLinkProgram(shader_program_); + glUseProgram(shader_program_); +} + +// Updates the OpenGL surface. +void GameEngine::onSurfaceChanged(int width, int height) { + width_ = width; + height_ = height; + + GLfloat heightIncrement = 0.25f; + GLfloat currentHeight = 0.93f; + + for (int i = 0; i < kNumberOfButtons; i++) { + int base = i * 8; + vertices_[base] = -0.9f; + vertices_[base + 1] = currentHeight; + vertices_[base + 2] = 0.9f; + vertices_[base + 3] = currentHeight; + vertices_[base + 4] = -0.9f; + vertices_[base + 5] = currentHeight - heightIncrement; + vertices_[base + 6] = 0.9f; + vertices_[base + 7] = currentHeight - heightIncrement; + currentHeight -= 1.2 * heightIncrement; + } +} + +// Draws the frame for the OpenGL surface. +void GameEngine::onDrawFrame() { + glClearColor(0.0f, 0.0f, bg_intensity_, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + GLuint vbo; + glGenBuffers(1, &vbo); + + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_), vertices_, GL_STATIC_DRAW); + + GLfloat colorBytes[] = {0.9f, 0.9f, 0.9f, 1.0f}; + GLint colorLocation = glGetUniformLocation(shader_program_, "myColor"); + glUniform4fv(colorLocation, 1, colorBytes); + + GLint posAttrib = glGetAttribLocation(shader_program_, "position"); + glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(posAttrib); + + for (int i = 0; i < kNumberOfButtons; i++) { + glDrawArrays(GL_TRIANGLE_STRIP, i * 4, 4); + } +} diff --git a/admob/tools/ios/testapp/testapp/game_engine.h b/admob/tools/ios/testapp/testapp/game_engine.h new file mode 100644 index 0000000000..7e3f062954 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/game_engine.h @@ -0,0 +1,74 @@ +// Copyright © 2016 Google. All rights reserved. + +#ifndef GAME_ENGINE_H_ +#define GAME_ENGINE_H_ + +#include +#include + +#include "firebase/admob.h" +#include "firebase/admob/banner_view.h" +#include "firebase/admob/interstitial_ad.h" +#include "firebase/admob/native_express_ad_view.h" +#include "firebase/admob/rewarded_video.h" +#include "firebase/admob/types.h" + +#ifndef __cplusplus +#error Header file supports C++ only +#endif // __cplusplus + +// Cross platform logging method. +extern "C" int LogMessage(const char* format, ...); + +class GameEngine { + static const int kNumberOfButtons = 6; + + // Set these flags to enable the ad formats that you want to test. + // BannerView and NativeExpressAdView share the same buttons for this testapp, + // so only one of these flags can be set to true when running the app. + static const bool kTestBannerView = true; + static const bool kTestNativeExpressAdView = false; + // InterstitialAd and rewarded_video:: share the same buttons for this + // testapp, so only one of these flags can be set to true when running the + // app. + static const bool kTestInterstitialAd = true; + static const bool kTestRewardedVideo = false; + + public: + GameEngine(); + + void Initialize(firebase::admob::AdParent ad_parent); + void onUpdate(); + void onTap(float x, float y); + void onSurfaceCreated(); + void onSurfaceChanged(int width, int height); + void onDrawFrame(); + + private: + firebase::admob::AdRequest createRequest(); + + firebase::admob::BannerView* banner_view_; + firebase::admob::NativeExpressAdView* native_express_view_; + firebase::admob::InterstitialAd* interstitial_ad_; + + bool banner_view_listener_set_; + bool native_express_ad_view_listener_set_; + bool interstitial_ad_listener_set_; + bool rewarded_video_listener_set_; + + firebase::admob::AdParent parent_view_; + firebase::admob::rewarded_video::PollableRewardListener* poll_listener_; + firebase::admob::rewarded_video::RewardItem reward_; + + bool bg_intensity_increasing_; + float bg_intensity_; + + GLuint vertex_shader_; + GLuint fragment_shader_; + GLuint shader_program_; + int height_; + int width_; + GLfloat vertices_[kNumberOfButtons * 8]; +}; + +#endif // GAME_ENGINE_H_ diff --git a/admob/tools/ios/testapp/testapp/main.m b/admob/tools/ios/testapp/testapp/main.m new file mode 100644 index 0000000000..35f5ab1db6 --- /dev/null +++ b/admob/tools/ios/testapp/testapp/main.m @@ -0,0 +1,11 @@ +// Copyright © 2016 Google. All rights reserved. + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } + +} diff --git a/analytics/generate_constants_test.py b/analytics/generate_constants_test.py new file mode 100644 index 0000000000..380b300a28 --- /dev/null +++ b/analytics/generate_constants_test.py @@ -0,0 +1,176 @@ +# Copyright 2016 Google LLC +# +# 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. + +"""Tests for generate_constants_lib.py.""" + +import datetime + +from google3.testing.pybase import googletest +from google3.firebase.analytics.client.cpp import generate_constants_lib + + +class GenerateHeaderTest(googletest.TestCase): + """Tests functions used to generate C++ header boilerplate.""" + + def test_cpp_header_guard(self): + """Verify header guards are formatted correctly.""" + self.assertEqual( + 'SOME_API_CPP_MYAPI_H_', + generate_constants_lib.cpp_header_guard('SOME_API_CPP_', 'myapi')) + + def test_format_cpp_header_header(self): + """Verify the header of C++ headers are formatted correctly.""" + self.assertEqual( + '// Copyright %s Google Inc. All Rights Reserved.\n' + '\n' + '#ifndef SOME_API_CPP_MYAPI_H_\n' + '#define SOME_API_CPP_MYAPI_H_\n' + '\n' + '/// @brief my package docs\n' + 'namespace mypackage {\n' + '/// @brief my api docs\n' + 'namespace myapi {\n' + '\n' % str(datetime.date.today().year), + generate_constants_lib.format_cpp_header_header( + 'SOME_API_CPP_', 'myapi.h', [('mypackage', 'my package docs'), + ('myapi', 'my api docs')])) + + def test_format_cpp_header_footer(self): + """Verify the footer of C++ headers are formatted correctly.""" + self.assertEqual( + '\n' + '} // namespace myapi\n' + '} // namespace mypackage\n' + '\n' + '#endif // SOME_API_CPP_MYAPI_H_\n', + generate_constants_lib.format_cpp_header_footer('SOME_API_CPP_', + 'myapi.h', + ['mypackage', 'myapi'])) + + +class DocStringParserTest(googletest.TestCase): + """Tests for DocStringParser.""" + + def test_parse_line(self): + """Test successfully parsing a line.""" + parser = generate_constants_lib.DocStringParser() + doc_line = '/// This is a test' + self.assertTrue(parser.parse_line(doc_line)) + self.assertListEqual([doc_line], parser.doc_string_lines) + + def test_parse_line_no_docs(self): + """Verify lines that don't contain docs are not parsed.""" + parser = generate_constants_lib.DocStringParser() + self.assertFalse(parser.parse_line( + 'static NSString *const test = @"test";')) + self.assertListEqual([], parser.doc_string_lines) + + def test_reset(self): + """Verify it's possible to reset the state of the parser.""" + parser = generate_constants_lib.DocStringParser() + self.assertTrue(parser.parse_line('/// This is a test')) + parser.reset() + self.assertListEqual([], parser.doc_string_lines) + + def test_apply_replacements(self): + """Test transformation of parsed doc strings.""" + parser = generate_constants_lib.DocStringParser(replacements=( + ('kT.XBish', 'kBish'), ('Bosh', ''), ('yo', 'hey'))) + self.assertEqual( + '/// This is a test of kBish', + generate_constants_lib.DocStringParser.apply_replacements( + '/// This is a test of kTTXBishBosh', + parser.replacements)) + + self.assertEqual( + '/// This is a hey of kBish hey', + generate_constants_lib.DocStringParser.apply_replacements( + '/// This is a yo of kTTXBishBosh yo', + parser.replacements, + replace_multiple_times=True)) + + def test_wrap_lines(self): + """Test line wrapping of parsed doc strings.""" + parser = generate_constants_lib.DocStringParser() + wrapped_lines = parser.wrap_lines( + ['/// this is a short paragraph', + '///', + '/// this is a' + (' very long line' * 10), + '///', + '/// more content', + '/// and some html that should not be wrapped', + '///

  • ', + '///
      some important stuff
    ', + '///
  • ', + '///
    ',
    +         '///   int some_code = "that should not"',
    +         '///                   "be wrapped"',
    +         '///                   "even' + (' long lines' * 10) + '";',
    +         '/// 
    ']) + self.assertListEqual( + ['/// this is a short paragraph', + '///', + '/// this is a very long line very long line very long line very ' + 'long line', + '/// very long line very long line very long line very long line ' + 'very long', + '/// line very long line', + '///', + '/// more content and some html that should not be wrapped', + '///
  • ', + '///
      some important stuff
    ', + '///
  • ', + '///
    ',
    +         '///   int some_code = "that should not"',
    +         '///                   "be wrapped"',
    +         '///                   "even long lines long lines long lines long '
    +         'lines long lines long lines long lines long lines long lines long '
    +         'lines";',
    +         '/// 
    '], + wrapped_lines) + + def test_paragraph_replacements(self): + """Test applying replacements to a paragraph.""" + parser = generate_constants_lib.DocStringParser( + paragraph_replacements=[('testy test', 'bishy bosh')]) + wrapped_lines = parser.wrap_lines(['/// testy', '/// test']) + self.assertListEqual(['/// bishy bosh'], wrapped_lines) + + def test_get_doc_string_lines(self): + """Test retrival of processed lines.""" + parser = generate_constants_lib.DocStringParser() + parser.parse_line('/// this is a test') + parser.parse_line('/// with two paragraphs') + parser.parse_line('///') + parser.parse_line('/// second paragraph') + self.assertListEqual( + ['/// this is a test with two paragraphs', + '///', + '/// second paragraph'], + parser.get_doc_string_lines()) + + def test_get_doc_string_empty(self): + """Verify an empty string is returned if no documentation is present.""" + parser = generate_constants_lib.DocStringParser() + self.assertEqual('', parser.get_doc_string()) + + def test_get_doc_string(self): + """Verify doc string terminated in a newline is returned.""" + parser = generate_constants_lib.DocStringParser() + parser.parse_line('/// this is a test') + self.assertEqual('/// this is a test\n', parser.get_doc_string()) + + +if __name__ == '__main__': + googletest.main() diff --git a/analytics/src_ios/fake/FIRAnalytics.h b/analytics/src_ios/fake/FIRAnalytics.h new file mode 100644 index 0000000000..060b712c6a --- /dev/null +++ b/analytics/src_ios/fake/FIRAnalytics.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#import + +@interface FIRAnalytics : NSObject + ++ (void)logEventWithName:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters; + ++ (void)setUserPropertyString:(nullable NSString *)value forName:(nonnull NSString *)name; + ++ (void)setUserID:(nullable NSString *)userID; + ++ (void)setScreenName:(nullable NSString *)screenName + screenClass:(nullable NSString *)screenClassOverride; + ++ (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled; + ++ (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval; + ++ (nullable NSString *)appInstanceID; + ++ (void)resetAnalyticsData; + +@end diff --git a/analytics/src_ios/fake/FIRAnalytics.mm b/analytics/src_ios/fake/FIRAnalytics.mm new file mode 100644 index 0000000000..6e2508f276 --- /dev/null +++ b/analytics/src_ios/fake/FIRAnalytics.mm @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#import "analytics/src_ios/fake/FIRAnalytics.h" + +#include "testing/reporter_impl.h" + +@implementation FIRAnalytics + ++ (NSString *)stringForValue:(id)value { + return [NSString stringWithFormat:@"%@", value]; +} + ++ (NSString *)stringForParameters:(NSDictionary *)parameters { + if ([parameters count] == 0) { + return @""; + } + + NSArray *sortedKeys = + [parameters.allKeys sortedArrayUsingSelector:@selector(compare:)]; + NSMutableString *parameterString = [NSMutableString string]; + for (NSString *key in sortedKeys) { + [parameterString appendString:key]; + [parameterString appendString:@"="]; + [parameterString appendString:[self stringForValue:parameters[key]]]; + [parameterString appendString:@","]; + } + // Remove trailing comma from string. + [parameterString deleteCharactersInRange:NSMakeRange([parameterString length] - 1, 1)]; + return parameterString; +} + ++ (void)logEventWithName:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters { + NSString *parameterString = [self stringForParameters:parameters]; + if (parameterString) { + FakeReporter->AddReport("+[FIRAnalytics logEventWithName:parameters:]", + { [name UTF8String], [parameterString UTF8String] }); + } else { + FakeReporter->AddReport("+[FIRAnalytics logEventWithName:parameters:]", + { [name UTF8String] }); + } +} + ++ (void)setUserPropertyString:(nullable NSString *)value forName:(nonnull NSString *)name { + FakeReporter->AddReport("+[FIRAnalytics setUserPropertyString:forName:]", + { [name UTF8String], value ? [value UTF8String] : "nil" }); +} + ++ (void)setUserID:(nullable NSString *)userID { + FakeReporter->AddReport("+[FIRAnalytics setUserID:]", { userID ? [userID UTF8String] : "nil" }); +} + ++ (void)setScreenName:(nullable NSString *)screenName + screenClass:(nullable NSString *)screenClassOverride { + FakeReporter->AddReport("+[FIRAnalytics setScreenName:screenClass:]", + { screenName ? [screenName UTF8String] : "nil", + screenClassOverride ? [screenClassOverride UTF8String] : "nil" }); +} + ++ (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval { + FakeReporter->AddReport( + "+[FIRAnalytics setSessionTimeoutInterval:]", + {[[NSString stringWithFormat:@"%.03f", sessionTimeoutInterval] UTF8String]}); +} + ++ (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled { + FakeReporter->AddReport("+[FIRAnalytics setAnalyticsCollectionEnabled:]", + {analyticsCollectionEnabled ? "YES" : "NO"}); +} + ++ (NSString *)appInstanceID { + FakeReporter->AddReport("+[FIRAnalytics appInstanceID]", {}); + return @"FakeAnalyticsInstanceId0"; +} + ++ (void)resetAnalyticsData { + FakeReporter->AddReport("+[FIRAnalytics resetAnalyticsData]", {}); +} + +@end diff --git a/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java b/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java new file mode 100644 index 0000000000..ef6f0495f8 --- /dev/null +++ b/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.analytics; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.FakeReporter; +import com.google.firebase.testing.cppsdk.TickerAndroid; + +import java.util.TreeSet; + +/** + * Fake for FirebaseAnalytics. + */ +public final class FirebaseAnalytics { + + public static FirebaseAnalytics getInstance(Context context) { + FakeReporter.addReport("FirebaseAnalytics.getInstance"); + return new FirebaseAnalytics(); + } + + public Task getAppInstanceId() { + FakeReporter.addReport("FirebaseAnalytics.getAppInstanceId"); + Task result = Task.forResult("FakeAnalyticsInstanceId0"); + TickerAndroid.register(result); + return result; + } + + public void setAnalyticsCollectionEnabled(boolean enabled) { + FakeReporter.addReport("FirebaseAnalytics.setAnalyticsCollectionEnabled", + Boolean.toString(enabled)); + } + + public void logEvent(String name, Bundle params) { + StringBuilder paramsString = new StringBuilder(); + // Sort keys for predictable ordering. + for (String key : new TreeSet<>(params.keySet())) { + paramsString.append(key); + paramsString.append("="); + paramsString.append(params.get(key)); + paramsString.append(","); + } + paramsString.setLength(Math.max(0, paramsString.length() - 1)); + FakeReporter.addReport("FirebaseAnalytics.logEvent", name, paramsString.toString()); + } + + public void resetAnalyticsData() { + FakeReporter.addReport("FirebaseAnalytics.resetAnalyticsData"); + } + + public void setUserProperty(String name, String value) { + FakeReporter.addReport("FirebaseAnalytics.setUserProperty", name, String.valueOf(value)); + } + + public void setCurrentScreen(Activity activity, String screenName, + String screenClassOverride) { + FakeReporter.addReport("FirebaseAnalytics.setCurrentScreen", activity.getClass().getName(), + String.valueOf(screenName), String.valueOf(screenClassOverride)); + } + + public void setUserId(String userId) { + FakeReporter.addReport("FirebaseAnalytics.setUserId", String.valueOf(userId)); + } + + public void setMinimumSessionDuration(long milliseconds) { + FakeReporter.addReport("FirebaseAnalytics.setMinimumSessionDuration", + Long.toString(milliseconds)); + } + + public void setSessionTimeoutDuration(long milliseconds) { + FakeReporter.addReport("FirebaseAnalytics.setSessionTimeoutDuration", + Long.toString(milliseconds)); + } + +} diff --git a/analytics/tests/CMakeLists.txt b/analytics/tests/CMakeLists.txt new file mode 100644 index 0000000000..51e72f2490 --- /dev/null +++ b/analytics/tests/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright 2019 Google LLC +# +# 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. + + + + +firebase_cpp_cc_test( + firebase_analytics_test + SOURCES + analytics_test.cc + DEPENDS + firebase_app_for_testing + firebase_analytics + firebase_testing +) + +firebase_cpp_cc_test_on_ios( + firebase_analytics_test + HOST + firebase_app_for_testing_ios + SOURCES + ${FIREBASE_SOURCE_DIR}/analytics/tests/analytics_test.cc + DEPENDS + firebase_app_for_testing + firebase_analytics + firebase_testing + "-lsqlite3" + CUSTOM_FRAMEWORKS + StoreKit +) diff --git a/analytics/tests/analytics_test.cc b/analytics/tests/analytics_test.cc new file mode 100644 index 0000000000..3e608cc447 --- /dev/null +++ b/analytics/tests/analytics_test.cc @@ -0,0 +1,310 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include + +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "analytics/src/analytics_common.h" +#include "analytics/src/include/firebase/analytics.h" +#include "app/src/include/firebase/app.h" +#include "app/src/time.h" +#include "app/tests/include/firebase/app_for_testing.h" + +#ifdef __ANDROID__ +#include "app/src/semaphore.h" +#include "app/src/util_android.h" +#endif // __ANDROID__ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace analytics { + +class AnalyticsTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + reporter_.reset(); + + firebase_app_ = testing::CreateApp(); + AddExpectationAndroid("FirebaseAnalytics.getInstance", {}); + analytics::Initialize(*firebase_app_); + } + + void TearDown() override { + firebase::testing::cppsdk::ConfigReset(); + Terminate(); + delete firebase_app_; + firebase_app_ = nullptr; + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + void AddExpectationAndroid(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kAndroid, + args); + } + + void AddExpectationApple(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kIos, args); + } + + // Wait for a task executing on the main thread. + void WaitForMainThreadTask() { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + Semaphore main_thread_signal(0); + util::RunOnMainThread( + firebase_app_->GetJNIEnv(), firebase_app_->activity(), + [](void* data) { reinterpret_cast(data)->Post(); }, + &main_thread_signal); + main_thread_signal.Wait(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + } + + // Wait for a future up to the specified number of milliseconds. + template + static void WaitForFutureWithTimeout(const Future& future, + int timeout_milliseconds, + FutureStatus expected_status) { + while (future.status() != expected_status && timeout_milliseconds-- > 0) { + ::firebase::internal::Sleep(1); + } + } + + App* firebase_app_ = nullptr; + + firebase::testing::cppsdk::Reporter reporter_; +}; + +TEST_F(AnalyticsTest, TestDestroyDefaultApp) { + EXPECT_TRUE(internal::IsInitialized()); + delete firebase_app_; + firebase_app_ = nullptr; + EXPECT_FALSE(internal::IsInitialized()); +} + +TEST_F(AnalyticsTest, TestSetAnalyticsCollectionEnabled) { + AddExpectationAndroid("FirebaseAnalytics.setAnalyticsCollectionEnabled", + {"true"}); + AddExpectationApple("+[FIRAnalytics setAnalyticsCollectionEnabled:]", + {"YES"}); + SetAnalyticsCollectionEnabled(true); +} + +TEST_F(AnalyticsTest, TestSetAnalyticsCollectionDisabled) { + AddExpectationAndroid("FirebaseAnalytics.setAnalyticsCollectionEnabled", + {"false"}); + AddExpectationApple("+[FIRAnalytics setAnalyticsCollectionEnabled:]", {"NO"}); + SetAnalyticsCollectionEnabled(false); +} + +TEST_F(AnalyticsTest, TestLogEventString) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=my_value"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=my_value"}); + + LogEvent("my_event", "my_param", "my_value"); +} + +TEST_F(AnalyticsTest, TestLogEventDouble) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=1.01"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=1.01"}); + LogEvent("my_event", "my_param", 1.01); +} + +TEST_F(AnalyticsTest, TestLogEventInt64) { + int64_t value = 101; + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=101"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=101"}); + + LogEvent("my_event", "my_param", value); +} + +TEST_F(AnalyticsTest, TestLogEventInt) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", "my_param=101"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "my_param=101"}); + + LogEvent("my_event", "my_param", 101); +} + +TEST_F(AnalyticsTest, TestLogEvent) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", {"my_event", ""}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", ""}); + + LogEvent("my_event"); +} + +TEST_F(AnalyticsTest, TestLogEvent40CharName) { + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"0123456789012345678901234567890123456789", ""}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"0123456789012345678901234567890123456789", ""}); + + LogEvent("0123456789012345678901234567890123456789"); +} + +TEST_F(AnalyticsTest, TestLogEventString40CharName) { + AddExpectationAndroid( + "FirebaseAnalytics.logEvent", + {"my_event", "0123456789012345678901234567890123456789=my_value"}); + AddExpectationApple( + "+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", "0123456789012345678901234567890123456789=my_value"}); + LogEvent("my_event", "0123456789012345678901234567890123456789", "my_value"); +} + +TEST_F(AnalyticsTest, TestLogEventString100CharValue) { + const std::string long_string = + "0123456789012345678901234567890123456789" + "012345678901234567890123456789012345678901234567890123456789"; + const std::string result = "my_event=" + long_string; + AddExpectationAndroid("FirebaseAnalytics.logEvent", + {"my_event", result.c_str()}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", result.c_str()}); + LogEvent("my_event", "my_event", long_string.c_str()); +} + +TEST_F(AnalyticsTest, TestLogEventParameters) { + // Params are sorted alphabetically by mock. + AddExpectationAndroid( + "FirebaseAnalytics.logEvent", + {"my_event", + "my_param_bool=1,my_param_double=1.01,my_param_int=101," + "my_param_string=my_value"}); + AddExpectationApple("+[FIRAnalytics logEventWithName:parameters:]", + {"my_event", + "my_param_bool=1,my_param_double=1.01,my_param_int=101," + "my_param_string=my_value"}); + + Parameter parameters[] = { + Parameter("my_param_string", "my_value"), + Parameter("my_param_double", 1.01), + Parameter("my_param_int", 101), + Parameter("my_param_bool", true), + }; + LogEvent("my_event", parameters, sizeof(parameters) / sizeof(parameters[0])); +} + +TEST_F(AnalyticsTest, TestSetUserProperty) { + AddExpectationAndroid("FirebaseAnalytics.setUserProperty", + {"my_property", "my_value"}); + AddExpectationApple("+[FIRAnalytics setUserPropertyString:forName:]", + {"my_property", "my_value"}); + + SetUserProperty("my_property", "my_value"); +} + +TEST_F(AnalyticsTest, TestSetUserPropertyNull) { + AddExpectationAndroid("FirebaseAnalytics.setUserProperty", + {"my_property", "null"}); + AddExpectationApple("+[FIRAnalytics setUserPropertyString:forName:]", + {"my_property", "nil"}); + SetUserProperty("my_property", nullptr); +} + +TEST_F(AnalyticsTest, TestSetUserId) { + AddExpectationAndroid("FirebaseAnalytics.setUserId", {"my_user_id"}); + AddExpectationApple("+[FIRAnalytics setUserID:]", {"my_user_id"}); + SetUserId("my_user_id"); +} + +TEST_F(AnalyticsTest, TestSetUserIdNull) { + AddExpectationAndroid("FirebaseAnalytics.setUserId", {"null"}); + AddExpectationApple("+[FIRAnalytics setUserID:]", {"nil"}); + SetUserId(nullptr); +} + +TEST_F(AnalyticsTest, TestSetSessionTimeoutDuration) { + AddExpectationAndroid("FirebaseAnalytics.setSessionTimeoutDuration", + {"1000"}); + AddExpectationApple("+[FIRAnalytics setSessionTimeoutInterval:]", {"1.000"}); + + SetSessionTimeoutDuration(1000); +} + +TEST_F(AnalyticsTest, TestSetCurrentScreen) { + AddExpectationAndroid("FirebaseAnalytics.setCurrentScreen", + {"android.app.Activity", "my_screen", "my_class"}); + AddExpectationApple("+[FIRAnalytics setScreenName:screenClass:]", + {"my_screen", "my_class"}); + + SetCurrentScreen("my_screen", "my_class"); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestSetCurrentScreenNullScreen) { + AddExpectationAndroid("FirebaseAnalytics.setCurrentScreen", + {"android.app.Activity", "null", "my_class"}); + AddExpectationApple("+[FIRAnalytics setScreenName:screenClass:]", + {"nil", "my_class"}); + + SetCurrentScreen(nullptr, "my_class"); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestSetCurrentScreenNullClass) { + AddExpectationAndroid("FirebaseAnalytics.setCurrentScreen", + {"android.app.Activity", "my_screen", "null"}); + AddExpectationApple("+[FIRAnalytics setScreenName:screenClass:]", + {"my_screen", "nil"}); + + SetCurrentScreen("my_screen", nullptr); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestResetAnalyticsData) { + AddExpectationAndroid("FirebaseAnalytics.resetAnalyticsData", {}); + AddExpectationApple("+[FIRAnalytics resetAnalyticsData]", {}); + AddExpectationApple("+[FIRAnalytics appInstanceID]", {}); + ResetAnalyticsData(); +} + +TEST_F(AnalyticsTest, TestGetAnalyticsInstanceId) { + AddExpectationAndroid("FirebaseAnalytics.getAppInstanceId", {}); + AddExpectationApple("+[FIRAnalytics appInstanceID]", {}); + auto result = GetAnalyticsInstanceId(); + // Wait for up to a second to fetch the ID. + WaitForFutureWithTimeout(result, 1000, firebase::kFutureStatusComplete); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(std::string("FakeAnalyticsInstanceId0"), *result.result()); +} + +} // namespace analytics +} // namespace firebase diff --git a/app/instance_id/instance_id_desktop_impl_test.cc b/app/instance_id/instance_id_desktop_impl_test.cc new file mode 100644 index 0000000000..3c46ddd188 --- /dev/null +++ b/app/instance_id/instance_id_desktop_impl_test.cc @@ -0,0 +1,819 @@ +// Copyright 2019 Google LLC +// +// 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 "app/instance_id/instance_id_desktop_impl.h" + +#include +#include +#include + +#include "app/rest/transport_mock.h" +#include "app/rest/util.h" +#include "app/rest/www_form_url_encoded.h" +#include "app/src/app_identifier.h" +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/future.h" +#include "app/src/include/firebase/version.h" +#include "app/src/log.h" +#include "app/src/secure/user_secure_manager_fake.h" +#include "app/src/time.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "third_party/jsoncpp/testing.h" + +namespace firebase { +namespace instance_id { +namespace internal { +using ::testing::MatchesRegex; +using ::testing::Ne; + +// Access fields and method from another class. Used because only the +// InstanceIdDesktopImplTest class has access, not any of the test case classes. +#define ACCESS_FIELD(object, name, field_type, field_name) \ + static void Set##name(object* impl, field_type value) { \ + impl->field_name = value; \ + } \ + static field_type Get##name(object* impl) { return impl->field_name; } + +#define ACCESS_METHOD0(object, method_return_type, method_name) \ + static method_return_type method_name(object* impl) { \ + return impl->method_name(); \ + } + +#define ACCESS_METHOD1(object, method_return_type, method_name, arg1_type) \ + static method_return_type method_name(object* impl, arg1_type arg1) { \ + return impl->method_name(arg1); \ + } + +#define ACCESS_METHOD2(object, method_return_type, method_name, arg1_type, \ + arg2_type) \ + static method_return_type method_name(object* impl, arg1_type arg1, \ + arg2_type arg2) { \ + return impl->method_name(arg1, arg2); \ + } + +#define SENDER_ID "55662211" +static const char kAppName[] = "app"; +static const char kStorageDomain[] = "iid_test"; +static const char kSampleProjectId[] = "sample_project_id"; +static const char kSamplePackageName[] = "sample.package.name"; +static const char kAppVersion[] = "5.6.7"; +static const char kOsVersion[] = "freedos-1.2.3"; +static const int kPlatform = 100; + +static const char kInstanceId[] = "test_instance_id"; +static const char kDeviceId[] = "test_device_id"; +static const char kSecurityToken[] = "test_security_token"; +static const uint64_t kLastCheckinTimeMs = 0x1234567890L; +static const char kDigest[] = "test_digest"; + +// Mock REST transport that validates request parameters. +class ValidatingTransportMock : public rest::TransportMock { + public: + struct ExpectedRequest { + ExpectedRequest() {} + + ExpectedRequest(const char* body_, bool body_is_json_, + const std::map& headers_) + : body(body_), body_is_json(body_is_json_), headers(headers_) {} + + std::string body; + bool body_is_json; + std::map headers; + }; + + ValidatingTransportMock() {} + + void SetExpectedRequestForUrl(const std::string& url, + const ExpectedRequest& expected) { + expected_request_by_url_[url] = expected; + } + + protected: + void PerformInternal( + rest::Request* request, rest::Response* response, + flatbuffers::unique_ptr* controller_out) override { + std::string body; + EXPECT_TRUE(request->ReadBodyIntoString(&body)); + + auto expected_it = expected_request_by_url_.find(request->options().url); + if (expected_it != expected_request_by_url_.end()) { + const ExpectedRequest& expected = expected_it->second; + if (expected.body_is_json) { + EXPECT_THAT(body, Json::testing::EqualsJson(expected.body)); + } else { + EXPECT_EQ(body, expected.body); + } + EXPECT_EQ(request->options().header, expected.headers); + } + + rest::TransportMock::PerformInternal(request, response, controller_out); + } + + private: + std::map expected_request_by_url_; +}; + +class InstanceIdDesktopImplTest : public ::testing::Test { + protected: + void SetUp() override { + LogSetLevel(kLogLevelDebug); + AppOptions options = testing::MockAppOptions(); + options.set_package_name(kSamplePackageName); + options.set_project_id(kSampleProjectId); + options.set_messaging_sender_id(SENDER_ID); + app_ = testing::CreateApp(options, kAppName); + impl_ = InstanceIdDesktopImpl::GetInstance(app_); + SetUserSecureManager( + impl_, + MakeUnique( + kStorageDomain, + firebase::internal::CreateAppIdentifierFromOptions(app_->options()) + .c_str())); + transport_ = new ValidatingTransportMock(); + SetTransport(impl_, UniquePtr(transport_)); + } + + void TearDown() override { + DeleteFromStorage(impl_); + delete impl_; + delete app_; + transport_ = nullptr; + } + + // Busy waits until |future| has completed. + void WaitForFuture(const FutureBase& future) { + ASSERT_THAT(future.status(), Ne(FutureStatus::kFutureStatusInvalid)); + while (true) { + if (future.status() != FutureStatus::kFutureStatusPending) { + break; + } + } + } + + // Create accessors / mutators for private fields in InstanceIdDesktopImpl. + ACCESS_FIELD(InstanceIdDesktopImpl, UserSecureManager, + UniquePtr, + user_secure_manager_); + ACCESS_FIELD(InstanceIdDesktopImpl, InstanceId, std::string, instance_id_); + typedef std::map TokenMap; + ACCESS_FIELD(InstanceIdDesktopImpl, Tokens, TokenMap, tokens_); + ACCESS_FIELD(InstanceIdDesktopImpl, Locale, std::string, locale_); + ACCESS_FIELD(InstanceIdDesktopImpl, Timezone, std::string, timezone_); + ACCESS_FIELD(InstanceIdDesktopImpl, LoggingId, int, logging_id_); + ACCESS_FIELD(InstanceIdDesktopImpl, IosDeviceModel, std::string, + ios_device_model_); + ACCESS_FIELD(InstanceIdDesktopImpl, IosDeviceVersion, std::string, + ios_device_version_); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataLastCheckinTimeMs, uint64_t, + checkin_data_.last_checkin_time_ms); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataSecurityToken, std::string, + checkin_data_.security_token); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataDeviceId, std::string, + checkin_data_.device_id); + ACCESS_FIELD(InstanceIdDesktopImpl, CheckinDataDigest, std::string, + checkin_data_.digest); + ACCESS_FIELD(InstanceIdDesktopImpl, AppVersion, std::string, app_version_); + ACCESS_FIELD(InstanceIdDesktopImpl, OsVersion, std::string, os_version_); + ACCESS_FIELD(InstanceIdDesktopImpl, Platform, int, platform_); + ACCESS_FIELD(InstanceIdDesktopImpl, Transport, UniquePtr, + transport_); + // Create wrappers for private methods in InstanceIdDesktopImpl. + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, SaveToStorage); + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, LoadFromStorage); + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, DeleteFromStorage); + ACCESS_METHOD0(InstanceIdDesktopImpl, bool, InitialOrRefreshCheckin); + ACCESS_METHOD0(InstanceIdDesktopImpl, std::string, GenerateAppId); + ACCESS_METHOD2(InstanceIdDesktopImpl, bool, FetchServerToken, const char*, + bool*); + ACCESS_METHOD2(InstanceIdDesktopImpl, bool, DeleteServerToken, const char*, + bool); + + InstanceIdDesktopImpl* impl_; + App* app_; + ValidatingTransportMock* transport_; +}; + +TEST_F(InstanceIdDesktopImplTest, TestInitialization) { + // Does everything initialize and delete properly? Checked automatically. +} + +TEST_F(InstanceIdDesktopImplTest, TestSaveAndLoad) { + SetInstanceId(impl_, kInstanceId); + SetCheckinDataLastCheckinTimeMs(impl_, kLastCheckinTimeMs); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataDigest(impl_, kDigest); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + + // Save to storage. + EXPECT_TRUE(SaveToStorage(impl_)); + + // Zero out the in-memory version so we need to load from storage. + SetInstanceId(impl_, ""); + SetCheckinDataLastCheckinTimeMs(impl_, 0); + SetCheckinDataDeviceId(impl_, ""); + SetCheckinDataSecurityToken(impl_, ""); + SetCheckinDataDigest(impl_, ""); + SetTokens(impl_, std::map()); + + // Make sure the data is zeroed out. + EXPECT_EQ("", GetInstanceId(impl_)); + EXPECT_EQ(0, GetCheckinDataLastCheckinTimeMs(impl_)); + EXPECT_EQ("", GetCheckinDataDeviceId(impl_)); + EXPECT_EQ("", GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ("", GetCheckinDataDigest(impl_)); + EXPECT_EQ(0, GetTokens(impl_).size()); + + // Load the data from storage. + EXPECT_TRUE(LoadFromStorage(impl_)); + + // Ensure that the loaded data is correct. + EXPECT_EQ(kInstanceId, GetInstanceId(impl_)); + EXPECT_EQ(kLastCheckinTimeMs, GetCheckinDataLastCheckinTimeMs(impl_)); + EXPECT_EQ(kDeviceId, GetCheckinDataDeviceId(impl_)); + EXPECT_EQ(kSecurityToken, GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ(kDigest, GetCheckinDataDigest(impl_)); + EXPECT_EQ(tokens, GetTokens(impl_)); + + EXPECT_TRUE(DeleteFromStorage(impl_)); + EXPECT_FALSE(LoadFromStorage(impl_)) + << "LoadFromStorage() should return false after deletion."; +} + +TEST_F(InstanceIdDesktopImplTest, TestGenerateAppId) { + const int kNumAppIds = 100; // Generate 100 AppIDs. + std::set generated_app_ids; + + for (int i = 0; i < kNumAppIds; ++i) { + std::string app_id = GenerateAppId(impl_); + + // AppIDs are always 11 bytes long. + EXPECT_EQ(app_id.length(), 11) << "Bad length: " << app_id; + + // AppIDs always start with c, d, e, or f, since the first 4 bits are 0x7. + EXPECT_TRUE(app_id[0] == 'c' || app_id[0] == 'd' || app_id[0] == 'e' || + app_id[0] == 'f') + << "Invalid first character: " << app_id; + + // AppIDs should only consist of [A-Za-z0-9_-] + EXPECT_THAT(app_id, MatchesRegex("^[A-Za-z0-9_-]*$")); + + // The same AppIDs should never be generated twice, so ensure no collision + // occurred. In theory this may be slightly flaky, but in practice if it + // actually collides with only 100 AppIDs, then we have a bigger problem. + EXPECT_TRUE(generated_app_ids.find(app_id) == generated_app_ids.end()) + << "Got an AppID collision: " << app_id; + generated_app_ids.insert(app_id); + } +} + +TEST_F(InstanceIdDesktopImplTest, CheckinFailure) { + // Backend returns an error. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 405 Method Not Allowed']," + " body: ['']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(InitialOrRefreshCheckin(impl_)); + + // Backend returns a malformed response. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['a bad response']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(InitialOrRefreshCheckin(impl_)); +} + +#define CHECKIN_SECURITY_TOKEN "123456789" +#define CHECKIN_DEVICE_ID "987654321" +#define CHECKIN_DIGEST "CA/fDTryF5eVxjNF8ZIJAg==" + +#define CHECKIN_RESPONSE_BODY \ + " {" \ + " \"device_data_version_info\":" \ + "\"DEVICE_DATA_VERSION_INFO_PLACEHOLDER\"," \ + " \"stats_ok\":\"1\"," \ + " \"security_token\":" CHECKIN_SECURITY_TOKEN \ + "," \ + " \"digest\":\"" CHECKIN_DIGEST \ + "\"," \ + " \"time_msec\":1557948713568," \ + " \"version_info\":\"0-qhPDIT2HYXIJ42qPW9kfDzoKzPqxY\"," \ + " \"android_id\":" CHECKIN_DEVICE_ID \ + "," \ + " \"intent\":[" \ + " {\"action\":\"com.google.android.gms.checkin.NOOP\"}" \ + " ]," \ + " \"setting\":[" \ + " {\"name\":\"android_id\"," \ + " \"value\":\"" CHECKIN_DEVICE_ID \ + "\"}," \ + " {\"name\":\"device_country\"," \ + " \"value\":\"us\"}," \ + " {\"name\":\"device_registration_time\"," \ + " \"value\":\"1557946800000\"}," \ + " {\"name\":\"ios_device\"," \ + " \"value\":\"1\"}" \ + " ]" \ + " }" + +TEST_F(InstanceIdDesktopImplTest, Checkin) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }" + " ]" + "}"); + +#define CHECKIN_TIMEZONE "America/Los_Angeles" +#define CHECKIN_LOCALE "en_US" +#define CHECKIN_IOS_DEVICE_MODEL "iPhone 8" +#define CHECKIN_IOS_DEVICE_VERSION "8.0" +#define CHECKIN_LOGGING_ID 11223344 + + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationJson; + headers[rest::util::kContentType] = rest::util::kApplicationJson; + transport_->SetExpectedRequestForUrl( + "https://device-provisioning.googleapis.com/checkin", + ValidatingTransportMock::ExpectedRequest( + "{ \"checkin\": " + "{ \"iosbuild\": " + "{ \"model\": \"" CHECKIN_IOS_DEVICE_MODEL "\", " + "\"os_version\": \"" CHECKIN_IOS_DEVICE_VERSION "\" }, " + "\"last_checkin_msec\": 0, \"type\": 2, \"user_number\": 0 }, " + "\"digest\": \"\", \"fragment\": 0, \"id\": 0, " + "\"locale\": \"en_US\", " + "\"logging_id\": " FIREBASE_STRING( + CHECKIN_LOGGING_ID) ", " + "\"security_token\": 0, \"timezone\": " + "\"" CHECKIN_TIMEZONE "\", " + "\"user_serial_number\": 0, \"version\": 2 }", + true, headers)); + SetLocale(impl_, CHECKIN_LOCALE); + SetTimezone(impl_, CHECKIN_TIMEZONE); + SetLoggingId(impl_, CHECKIN_LOGGING_ID); + SetIosDeviceModel(impl_, CHECKIN_IOS_DEVICE_MODEL); + SetIosDeviceVersion(impl_, CHECKIN_IOS_DEVICE_VERSION); + EXPECT_TRUE(InitialOrRefreshCheckin(impl_)); + + // Make sure the logged checkin time is within a second. + EXPECT_LT(firebase::internal::GetTimestamp() - + GetCheckinDataLastCheckinTimeMs(impl_), + 1000); + // Check the cached check-in data. + EXPECT_EQ(CHECKIN_SECURITY_TOKEN, GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ(CHECKIN_DEVICE_ID, GetCheckinDataDeviceId(impl_)); + EXPECT_EQ(CHECKIN_DIGEST, GetCheckinDataDigest(impl_)); + + // Try checking in again, this should do nothing as the credentials haven't + // expired. + firebase::testing::cppsdk::ConfigSet("{}"); + transport_->SetExpectedRequestForUrl( + "https://device-provisioning.googleapis.com/checkin", + ValidatingTransportMock::ExpectedRequest()); + EXPECT_TRUE(InitialOrRefreshCheckin(impl_)); + // Make sure the cached check-in data didn't change. + EXPECT_EQ(CHECKIN_SECURITY_TOKEN, GetCheckinDataSecurityToken(impl_)); + EXPECT_EQ(CHECKIN_DEVICE_ID, GetCheckinDataDeviceId(impl_)); + EXPECT_EQ(CHECKIN_DIGEST, GetCheckinDataDigest(impl_)); + +#undef CHECKIN_TIMEZONE +#undef CHECKIN_LOCALE +#undef CHECKIN_IOS_DEVICE_MODEL +#undef CHECKIN_IOS_DEVICE_VERSION +#undef CHECKIN_LOGGING_ID +#undef CHECKIN_LOGGING_ID_STRING +} + +#define FETCH_TOKEN "atoken" + +TEST_F(InstanceIdDesktopImplTest, FetchServerToken) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }," + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['token=" FETCH_TOKEN + "']" + " }" + " }" + " ]" + "}"); + + // Set token fetch parameters. + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + // TODO(smiles): The following two lines should be removed when we're + // generating the IID. + SetInstanceId(impl_, kInstanceId); + SaveToStorage(impl_); + std::string expected_request; + { + rest::WwwFormUrlEncoded form(&expected_request); + form.Add("sender", SENDER_ID); + form.Add("app", kSamplePackageName); + form.Add("app_ver", kAppVersion); + form.Add("device", CHECKIN_DEVICE_ID); + form.Add("X-scope", "*"); + form.Add("X-subtype", SENDER_ID); + form.Add("X-osv", kOsVersion); + form.Add("plat", "100"); + form.Add("app_id", kInstanceId); + } + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kContentType] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kAuthorization] = + std::string("AidLogin ") + std::string(CHECKIN_DEVICE_ID) + + std::string(":") + std::string(CHECKIN_SECURITY_TOKEN); + transport_->SetExpectedRequestForUrl( + "https://fcmtoken.googleapis.com/register", + ValidatingTransportMock::ExpectedRequest(expected_request.c_str(), false, + headers)); + bool retry; + EXPECT_TRUE(FetchServerToken(impl_, "*", &retry)); + EXPECT_FALSE(retry); + + std::map expected_tokens; + expected_tokens["*"] = FETCH_TOKEN; + EXPECT_EQ(expected_tokens, GetTokens(impl_)); +} + +TEST_F(InstanceIdDesktopImplTest, FetchServerTokenRegistrationError) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }," + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['Error=PHONE_REGISTRATION_ERROR&token=" FETCH_TOKEN + "sender=55662211" + "']" + " }" + " }" + " ]" + "}"); + + // Set token fetch parameters. + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + // TODO(smiles): The following two lines should be removed when we're + // generating the IID. + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = FETCH_TOKEN; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + bool retry; + EXPECT_FALSE(FetchServerToken(impl_, "fcm", &retry)); + EXPECT_TRUE(retry); + EXPECT_EQ(1, GetTokens(impl_).size()); +} + +TEST_F(InstanceIdDesktopImplTest, FetchServerTokenExpired) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://device-provisioning.googleapis.com/checkin'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['" CHECKIN_RESPONSE_BODY + "']" + " }" + " }," + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['Error=foo%3Abar%3Aother%20stuff%3ARST&token=" FETCH_TOKEN + "sender=55662211" + "']" + " }" + " }" + " ]" + "}"); + + // Set token fetch parameters. + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + // TODO(smiles): The following two lines should be removed when we're + // generating the IID. + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = FETCH_TOKEN; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + bool retry; + EXPECT_FALSE(FetchServerToken(impl_, "fcm", &retry)); + EXPECT_FALSE(retry); + EXPECT_EQ(0, GetTokens(impl_).size()); +} + +#undef FETCH_TOKEN +#undef CHECKIN_SECURITY_TOKEN +#undef CHECKIN_DEVICE_ID +#undef CHECKIN_DIGEST +#undef CHECKIN_RESPONSE_BODY + +TEST_F(InstanceIdDesktopImplTest, FetchServerTokenFailure) { + // Backend returns an error. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 405 Method Not Allowed']," + " body: ['']" + " }" + " }" + " ]" + "}"); + bool retry; + EXPECT_FALSE(FetchServerToken(impl_, "*", &retry)); + EXPECT_FALSE(retry); + EXPECT_EQ(0, GetTokens(impl_).size()); + + // Backend returns an invalid response. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['foo=bar&wibble=wobble']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(FetchServerToken(impl_, "*", &retry)); + EXPECT_FALSE(retry); + EXPECT_EQ(0, GetTokens(impl_).size()); +} + +TEST_F(InstanceIdDesktopImplTest, DeleteServerTokenNoop) { + // Deleting a token that doesn't exist should succeed. + EXPECT_TRUE(DeleteServerToken(impl_, nullptr, true)); + EXPECT_TRUE(DeleteServerToken(impl_, "fcm", false)); +} + +TEST_F(InstanceIdDesktopImplTest, DeleteServerToken) { + const char* kResponses[] = { + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['token=" SENDER_ID + "']" + " }" + " }" + " ]" + "}", + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['deleted=" SENDER_ID + "']" + " }" + " }" + " ]" + "}", + }; + for (size_t i = 0; i < sizeof(kResponses) / sizeof(kResponses[0]); ++i) { + firebase::testing::cppsdk::ConfigSet(kResponses[i]); + + std::string expected_request; + { + rest::WwwFormUrlEncoded form(&expected_request); + form.Add("sender", SENDER_ID); + form.Add("app", kSamplePackageName); + form.Add("app_ver", kAppVersion); + form.Add("device", kDeviceId); + form.Add("X-scope", "fcm"); + form.Add("X-subtype", SENDER_ID); + form.Add("X-osv", kOsVersion); + form.Add("plat", "100"); + form.Add("app_id", kInstanceId); + form.Add("delete", "true"); + } + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kContentType] = + rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kAuthorization] = + std::string("AidLogin ") + std::string(kDeviceId) + std::string(":") + + std::string(kSecurityToken); + + transport_->SetExpectedRequestForUrl( + "https://fcmtoken.googleapis.com/register", + ValidatingTransportMock::ExpectedRequest(expected_request.c_str(), + false, headers)); + + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataLastCheckinTimeMs(impl_, firebase::internal::GetTimestamp()); + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + EXPECT_TRUE(DeleteServerToken(impl_, "fcm", false)) << "Iteration " << i; + + std::map expected_tokens; + expected_tokens["*"] = "123456789"; + EXPECT_EQ(expected_tokens, GetTokens(impl_)); + + // Clean up storage before the next iteration. + DeleteFromStorage(impl_); + } +} + +TEST_F(InstanceIdDesktopImplTest, DeleteTokenFailed) { + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataLastCheckinTimeMs(impl_, firebase::internal::GetTimestamp()); + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + // Delete a token that isn't present. + EXPECT_TRUE(DeleteServerToken(impl_, "non-existent-token", false)); + EXPECT_EQ(tokens, GetTokens(impl_)); + + // Delete a token with a server failure. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 405 Method Not Allowed']," + " body: ['']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(DeleteServerToken(impl_, "fcm", false)); + EXPECT_EQ(tokens, GetTokens(impl_)); + + // Delete a token with an invalid server response. + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['everything is just fine']" + " }" + " }" + " ]" + "}"); + EXPECT_FALSE(DeleteServerToken(impl_, "fcm", false)); + EXPECT_EQ(tokens, GetTokens(impl_)); +} + +TEST_F(InstanceIdDesktopImplTest, DeleteAllServerTokens) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config: [" + " {" + " fake: 'https://fcmtoken.googleapis.com/register'," + " httpresponse: {" + " header: ['HTTP/1.1 200 OK']," + " body: ['token=" SENDER_ID + "']" + " }" + " }" + " ]" + "}"); + + std::string expected_request; + { + rest::WwwFormUrlEncoded form(&expected_request); + form.Add("sender", SENDER_ID); + form.Add("app", kSamplePackageName); + form.Add("app_ver", kAppVersion); + form.Add("device", kDeviceId); + form.Add("X-scope", "*"); + form.Add("X-subtype", SENDER_ID); + form.Add("X-osv", kOsVersion); + form.Add("plat", "100"); + form.Add("app_id", kInstanceId); + form.Add("delete", "true"); + form.Add("iid-operation", "delete"); + } + std::map headers; + headers[rest::util::kAccept] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kContentType] = rest::util::kApplicationWwwFormUrlencoded; + headers[rest::util::kAuthorization] = + std::string("AidLogin ") + std::string(kDeviceId) + std::string(":") + + std::string(kSecurityToken); + + transport_->SetExpectedRequestForUrl( + "https://fcmtoken.googleapis.com/register", + ValidatingTransportMock::ExpectedRequest(expected_request.c_str(), false, + headers)); + + SetAppVersion(impl_, kAppVersion); + SetOsVersion(impl_, kOsVersion); + SetPlatform(impl_, kPlatform); + SetCheckinDataDeviceId(impl_, kDeviceId); + SetCheckinDataSecurityToken(impl_, kSecurityToken); + SetCheckinDataLastCheckinTimeMs(impl_, firebase::internal::GetTimestamp()); + SetInstanceId(impl_, kInstanceId); + std::map tokens; + tokens["*"] = "123456789"; + tokens["fcm"] = "987654321"; + SetTokens(impl_, tokens); + SaveToStorage(impl_); + + EXPECT_TRUE(DeleteServerToken(impl_, nullptr, true)); + EXPECT_EQ(0, GetTokens(impl_).size()); +} + +} // namespace internal +} // namespace instance_id +} // namespace firebase diff --git a/app/memory/atomic_test.cc b/app/memory/atomic_test.cc new file mode 100644 index 0000000000..8ecea4e9b9 --- /dev/null +++ b/app/memory/atomic_test.cc @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/memory/atomic.h" + +#include // NOLINT +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +// Basic sanity tests for atomic operations. + +namespace firebase { +namespace compat { +namespace { + +using ::testing::Eq; + +const uint64_t kValue = 10; +const uint64_t kUpdatedValue = 20; + +TEST(AtomicTest, DefaultConstructedAtomicIsEqualToZero) { + Atomic atomic; + EXPECT_THAT(atomic.load(), Eq(0)); +} + +TEST(AtomicTest, AssignedValueIsProperlyLoadedViaLoad) { + Atomic atomic(kValue); + EXPECT_THAT(atomic.load(), Eq(kValue)); +} + +TEST(AtomicTest, FetchAddProperlyAddsValueAndReturnsValueBeforeAddition) { + Atomic atomic(kValue); + EXPECT_THAT(atomic.fetch_add(kValue), kValue); + EXPECT_THAT(atomic.load(), Eq(2 * kValue)); +} + +TEST(AtomicTest, + FetchSubProperlySubtractsValueAndReturnsValueBeforeSubtraction) { + Atomic atomic(kValue); + EXPECT_THAT(atomic.fetch_sub(kValue), kValue); + EXPECT_THAT(atomic.load(), Eq(0)); +} + +TEST(AtomicTest, NewValueIsProperlyAssignedWithAssignmentOperator) { + Atomic atomic; + atomic = kValue; + EXPECT_THAT(atomic.load(), Eq(kValue)); +} + +// Note: This test needs to spin and can't use synchronization like +// mutex+condvar because their use renders the test useless due to the fact that +// in the presence of synchronization non-atomic updates are also guaranteed to +// be visible across threads. +TEST(AtomicTest, AtomicUpdatesAreVisibleAcrossThreads) { + Atomic atomic(kValue); + + std::thread thread([&atomic]() { + while (atomic.load() == kValue) { + } + atomic.fetch_add(1); + }); + atomic.store(kUpdatedValue); + thread.join(); + + EXPECT_THAT(atomic.load(), Eq(kUpdatedValue + 1)); +} + +TEST(AtomicTest, AtomicUpdatesAreVisibleAcrossMultipleThreads) { + Atomic atomic; + + const int num_threads = 10; + std::vector threads; + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back([&atomic] { atomic.fetch_add(1); }); + } + for (auto& thread : threads) { + thread.join(); + } + EXPECT_THAT(atomic.load(), Eq(num_threads)); +} + +} // namespace +} // namespace compat +} // namespace firebase diff --git a/app/memory/shared_ptr_test.cc b/app/memory/shared_ptr_test.cc new file mode 100644 index 0000000000..7f76b816b0 --- /dev/null +++ b/app/memory/shared_ptr_test.cc @@ -0,0 +1,229 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/memory/shared_ptr.h" + +#include // NOLINT +#include + +#include "app/memory/atomic.h" +#include "app/meta/move.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::firebase::compat::Atomic; +using ::testing::Eq; +using ::testing::IsNull; + +class Destructable { + public: + explicit Destructable(Atomic* destroyed) : destroyed_(destroyed) {} + virtual ~Destructable() { destroyed_->fetch_add(1); } + + private: + Atomic* const destroyed_; +}; + +class Derived : public Destructable { + public: + explicit Derived(Atomic* destroyed) : Destructable(destroyed) {} +}; + +TEST(SharedPtrTest, DefaultConstructedSharedPtrDoesNotManageAnObject) { + SharedPtr ptr; + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); +} + +TEST(SharedPtrTest, EmptySharedPtrCopiesDoNotManageAnObject) { + SharedPtr ptr; + SharedPtr ptr2(ptr); // NOLINT + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr2.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); + EXPECT_THAT(ptr2.get(), Eq(nullptr)); +} + +TEST(SharedPtrTest, NullptrConstructedSharedPtrDoesNotManageAnObject) { + SharedPtr ptr(static_cast(nullptr)); + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); +} + +TEST(SharedPtrTest, WrapSharedCreatesValidSharedPtr) { + Atomic destroyed; + { + auto destructable = new Destructable(&destroyed); + auto ptr = WrapShared(destructable); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, SharedPtrCorrectlyDestroysTheContainedObject) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, CopiesShareTheSameObjectWhichIsDestroyedOnlyOnce) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + auto ptr2 = ptr; // NOLINT + EXPECT_THAT(ptr.use_count(), Eq(2)); + EXPECT_THAT(ptr.get(), Eq(ptr2.get())); + } + EXPECT_THAT(ptr.use_count(), Eq(1)); + EXPECT_THAT(destroyed.load(), Eq(0)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, MoveCorrectlyTransfersOwnership) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + auto* managed = ptr.get(); + auto ptr2 = Move(ptr); + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); + EXPECT_THAT(ptr2.use_count(), Eq(1)); + EXPECT_THAT(ptr2.get(), Eq(managed)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, + ConvertingCopiesShareTheSameObjectWhichIsDestroyedOnlyOnce) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + SharedPtr ptr2(ptr); // NOLINT + EXPECT_THAT(ptr.use_count(), Eq(2)); + EXPECT_THAT(ptr.get(), Eq(ptr2.get())); + } + EXPECT_THAT(ptr.use_count(), Eq(1)); + EXPECT_THAT(destroyed.load(), Eq(0)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, ConvertingMoveCorrectlyTransfersOwnership) { + Atomic destroyed; + { + auto ptr = MakeShared(&destroyed); + EXPECT_THAT(ptr.use_count(), Eq(1)); + { + auto* managed = ptr.get(); + SharedPtr ptr2(Move(ptr)); + EXPECT_THAT(ptr.use_count(), Eq(0)); + EXPECT_THAT(ptr.get(), Eq(nullptr)); + EXPECT_THAT(ptr2.use_count(), Eq(1)); + EXPECT_THAT(ptr2.get(), Eq(managed)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, EmptySharedPtrIsFalseWhenConvertedToBool) { + SharedPtr ptr; + EXPECT_FALSE(ptr); +} + +TEST(SharedPtrTest, NontEmptySharedPtrIsTrueWhenConvertedToBool) { + auto ptr = MakeShared(1); + EXPECT_TRUE(ptr); +} + +TEST(SharedPtrTest, + SharedPtrRefCountIsThreadSafeAndOnlyDeletesTheManagedPtrOnce) { + Atomic destroyed; + std::vector threads; + { + auto ptr = MakeShared(&destroyed); + + for (int i = 0; i < 10; ++i) { + threads.emplace_back([ptr] { + // make another copy. + auto ptr2 = ptr; // NOLINT + }); + } + EXPECT_THAT(destroyed.load(), Eq(0)); + } + for (auto& thread : threads) { + thread.join(); + } + EXPECT_THAT(destroyed.load(), Eq(1)); +} + +TEST(SharedPtrTest, CopySharedPtr) { + SharedPtr *value1 = new SharedPtr(new int(10)); + SharedPtr *value2 = new SharedPtr(); + *value2 = *value1; + delete value1; + EXPECT_THAT(**value2, 10); + delete value2; +} + +TEST(SharedPtrTest, CopySharedPtrDereferenceTest) { + SharedPtr ptr1 = MakeShared(10); + SharedPtr ptr2 = MakeShared(10); + SharedPtr ptr3 = MakeShared(10); + SharedPtr ptr = ptr1; + ptr = ptr2; + EXPECT_THAT(ptr1.use_count(), Eq(1)); + ptr = ptr3; + EXPECT_THAT(ptr2.use_count(), Eq(1)); +} + +TEST(SharedPtrTest, SharedPtrReset) { + SharedPtr ptr1 = MakeShared(10); + ptr1.reset(); + EXPECT_THAT(ptr1.get(), IsNull()); // NOLINT + + SharedPtr ptr2 = MakeShared(10); + SharedPtr ptr3 = ptr2; + ptr3.reset(); + EXPECT_THAT(ptr3.get(), IsNull()); // NOLINT + EXPECT_THAT(ptr2.use_count(), Eq(1)); +} + +TEST(SharedPtrTest, MoveSharedPtr) { + SharedPtr value1(new int(10)); + SharedPtr value2; + EXPECT_THAT(*value1, Eq(10)); + value2 = Move(value1); + EXPECT_THAT(value1.get(), IsNull()); // NOLINT + EXPECT_THAT(*value2, Eq(10)); +} + +} // namespace +} // namespace firebase diff --git a/app/memory/unique_ptr_test.cc b/app/memory/unique_ptr_test.cc new file mode 100644 index 0000000000..541a89d9e6 --- /dev/null +++ b/app/memory/unique_ptr_test.cc @@ -0,0 +1,174 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/memory/unique_ptr.h" + +#include "app/meta/move.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::testing::Eq; + +typedef void (*OnDestroyFn)(bool*); + +class Destructable { + public: + explicit Destructable(bool* destroyed) : destroyed_(destroyed) {} + ~Destructable() { *destroyed_ = true; } + + bool destroyed() const { return destroyed_; } + + private: + bool* const destroyed_; +}; + +class Base { + public: + virtual ~Base() {} +}; + +class Derived : public Base { + public: + Derived(bool* destroyed) : destroyed_(destroyed) {} + ~Derived() override { *destroyed_ = true; } + bool destroyed() const { return destroyed_; } + + private: + bool* const destroyed_; +}; + +void Foo(UniquePtr b) {} + +void AssertRawPtrEq(const UniquePtr& ptr, Destructable* value) { + ASSERT_THAT(ptr.get(), Eq(value)); + ASSERT_THAT(ptr.operator->(), Eq(value)); + + if (value != nullptr) { + ASSERT_THAT((*ptr).destroyed(), Eq(value->destroyed())); + ASSERT_THAT(ptr->destroyed(), Eq(value->destroyed())); + } +} + +TEST(UniquePtrTest, DeletesContainingPtrWhenDestroyed) { + bool destroyed = false; + { MakeUnique(&destroyed); } + EXPECT_THAT(destroyed, Eq(true)); +} + +TEST(UniquePtrTest, DoesNotDeleteContainingPtrWhenDestroyedIfReleased) { + bool destroyed = false; + Destructable* raw_ptr; + { + auto ptr = MakeUnique(&destroyed); + raw_ptr = ptr.release(); + } + EXPECT_THAT(destroyed, Eq(false)); + delete raw_ptr; + EXPECT_THAT(destroyed, Eq(true)); +} + +TEST(UniquePtrTest, MoveConstructionTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed = false; + { + auto ptr = MakeUnique(&destroyed); + auto* raw_ptr = ptr.get(); + auto movedInto = UniquePtr(Move(ptr)); + + AssertRawPtrEq(ptr, nullptr); + AssertRawPtrEq(movedInto, raw_ptr); + } +} + +TEST(UniquePtrTest, CopyConstructionTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed = false; + { + auto ptr = MakeUnique(&destroyed); + auto* raw_ptr = ptr.get(); + auto movedInto = UniquePtr(ptr); + + AssertRawPtrEq(ptr, nullptr); + AssertRawPtrEq(movedInto, raw_ptr); + } +} + +TEST(UniquePtrTest, MoveAssignmentTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed1 = false; + bool destroyed2 = false; + { + auto ptr1 = MakeUnique(&destroyed1); + auto ptr2 = MakeUnique(&destroyed2); + + auto* raw_ptr2 = ptr2.get(); + ptr1 = Move(ptr2); + + ASSERT_THAT(destroyed1, Eq(true)); + AssertRawPtrEq(ptr1, raw_ptr2); + AssertRawPtrEq(ptr2, nullptr); + } + ASSERT_THAT(destroyed2, Eq(true)); +} + +TEST(UniquePtrTest, CopyAssignmentTransfersOwnershipOfTheUnderlyingPtr) { + bool destroyed1 = false; + bool destroyed2 = false; + { + auto ptr1 = MakeUnique(&destroyed1); + auto ptr2 = MakeUnique(&destroyed2); + + auto* raw_ptr2 = ptr2.get(); + ptr1 = ptr2; + + ASSERT_THAT(destroyed1, Eq(true)); + AssertRawPtrEq(ptr1, raw_ptr2); + AssertRawPtrEq(ptr2, nullptr); + } + ASSERT_THAT(destroyed2, Eq(true)); +} + +TEST(UniquePtrTest, MoveAssignmentToEmptyTransfersOwnershipOfThePtr) { + bool destroyed = false; + { + UniquePtr ptr; + AssertRawPtrEq(ptr, nullptr); + + auto raw_ptr = new Destructable(&destroyed); + ptr = raw_ptr; + AssertRawPtrEq(ptr, raw_ptr); + } + ASSERT_THAT(destroyed, Eq(true)); +} + +TEST(UniquePtrTest, EmptyUniquePtrImplicitlyConvertsToFalse) { + UniquePtr ptr; + EXPECT_THAT(ptr, Eq(false)); +} + +TEST(UniquePtrTest, NonEmptyUniquePtrImplicitlyConvertsToTrue) { + auto ptr = MakeUnique(10); + EXPECT_THAT(ptr, Eq(true)); +} + +TEST(UniquePtrTest, UniquePtrToDerivedConvertsToBase) { + bool destroyed = false; + { UniquePtr base_ptr = MakeUnique(&destroyed); } + EXPECT_THAT(destroyed, Eq(true)); +} + +} // namespace +} // namespace firebase diff --git a/app/meta/move_test.cc b/app/meta/move_test.cc new file mode 100644 index 0000000000..3b947f2a04 --- /dev/null +++ b/app/meta/move_test.cc @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/meta/move.h" + +#include "app/src/include/firebase/internal/type_traits.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::testing::Eq; + +class MoveTester { + public: + MoveTester() = default; + MoveTester(const MoveTester&) = default; + MoveTester(MoveTester&& other) : moved_(true) {} + MoveTester& operator=(MoveTester&& other) { + moved_ = true; + return *this; + } + bool moved() const { return moved_; } + + private: + bool moved_ = false; +}; + +TEST(MoveTest, DefaultConstructedMoveTesterIsNotMoved) { + MoveTester tester; + ASSERT_THAT(tester.moved(), Eq(false)); +} + +TEST(MoveTest, CopyConstructedMoveTesterIsNotMoved) { + MoveTester tester; + MoveTester copiedTester(tester); + ASSERT_THAT(copiedTester.moved(), Eq(false)); +} + +TEST(MoveTest, MoveConstructedMoveTesterIsMoved) { + MoveTester tester; + MoveTester copiedTester(Move(tester)); + ASSERT_THAT(copiedTester.moved(), Eq(true)); +} + +TEST(MoveTest, MoveAssignedMoveTesterIsMoved) { + MoveTester tester1; + MoveTester tester2; + tester2 = Move(tester1); + ASSERT_THAT(tester2.moved(), Eq(true)); +} + +} // namespace +} // namespace firebase diff --git a/app/rest/tests/gzipheader_unittest.cc b/app/rest/tests/gzipheader_unittest.cc new file mode 100644 index 0000000000..b95375462a --- /dev/null +++ b/app/rest/tests/gzipheader_unittest.cc @@ -0,0 +1,162 @@ +// +// Copyright 2003 Google LLC All rights reserved. +// +// 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. +// +// Author: Neal Cardwell + +#include "app/rest/gzipheader.h" + +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "absl/base/macros.h" +#include "absl/strings/escaping.h" +#include "util/random/acmrandom.h" + +namespace firebase { + +// Take some test headers and pass them to a GZipHeader, fragmenting +// the headers in many different random ways. +TEST(GzipHeader, FragmentTest) { + ACMRandom rnd(ACMRandom::DeprecatedDefaultSeed()); + + struct TestCase { + const char* str; + int len; // total length of the string + int cruft_len; // length of the gzip header part + }; + TestCase tests[] = { + // Basic header: + {"\037\213\010\000\216\176\356\075\002\003", 10, 0}, + + // Basic headers with crud on the end: + {"\037\213\010\000\216\176\356\075\002\003X", 11, 1}, + {"\037\213\010\000\216\176\356\075\002\003XXX", 13, 3}, + + { + "\037\213\010\010\321\135\265\100\000\003" + "emacs\000", + 16, 0 // with an FNAME of "emacs" + }, + { + "\037\213\010\010\321\135\265\100\000\003" + "\000", + 11, 0 // with an FNAME of zero bytes + }, + { + "\037\213\010\020\321\135\265\100\000\003" + "emacs\000", + 16, 0, // with an FCOMMENT of "emacs" + }, + { + "\037\213\010\020\321\135\265\100\000\003" + "\000", + 11, 0, // with an FCOMMENT of zero bytes + }, + { + "\037\213\010\002\321\135\265\100\000\003" + "\001\002", + 12, 0 // with an FHCRC + }, + { + "\037\213\010\004\321\135\265\100\000\003" + "\003\000foo", + 15, 0 // with an extra of "foo" + }, + { + "\037\213\010\004\321\135\265\100\000\003" + "\000\000", + 12, 0 // with an extra of zero bytes + }, + { + "\037\213\010\032\321\135\265\100\000\003" + "emacs\000" + "emacs\000" + "\001\002", + 24, 0 // with an FNAME of "emacs", FCOMMENT of "emacs", and FHCRC + }, + { + "\037\213\010\036\321\135\265\100\000\003" + "\003\000foo" + "emacs\000" + "emacs\000" + "\001\002", + 29, 0 // with an FNAME of "emacs", FCOMMENT of "emacs", FHCRC, "foo" + }, + { + "\037\213\010\036\321\135\265\100\000\003" + "\003\000foo" + "emacs\000" + "emacs\000" + "\001\002" + "XXX", + 32, 3 // FNAME of "emacs", FCOMMENT of "emacs", FHCRC, "foo", crud + }, + }; + + // Test all the headers test cases. + for (int i = 0; i < ABSL_ARRAYSIZE(tests); ++i) { + // Test many random ways they might be fragmented. + for (int j = 0; j < 100 * 1000; ++j) { + // Get the test case set up. + const char* p = tests[i].str; + int bytes_left = tests[i].len; + int bytes_read = 0; + + // Pick some random places to fragment the headers. + const int num_fragments = rnd.Uniform(bytes_left); + std::vector fragment_starts; + for (int frag_num = 0; frag_num < num_fragments; ++frag_num) { + fragment_starts.push_back(rnd.Uniform(bytes_left)); + } + std::sort(fragment_starts.begin(), fragment_starts.end()); + + VLOG(1) << "====="; + GZipHeader gzip_headers; + // Go through several fragments and pass them to the headers for parsing. + int frag_num = 0; + while (bytes_left > 0) { + const int fragment_len = (frag_num < num_fragments) + ? (fragment_starts[frag_num] - bytes_read) + : (tests[i].len - bytes_read); + CHECK_GE(fragment_len, 0); + const char* header_end = NULL; + VLOG(1) << absl::StrFormat("Passing %2d bytes at %2d..%2d: %s", + fragment_len, bytes_read, + bytes_read + fragment_len, + absl::CEscape(std::string(p, fragment_len))); + GZipHeader::Status status = + gzip_headers.ReadMore(p, fragment_len, &header_end); + bytes_read += fragment_len; + bytes_left -= fragment_len; + CHECK_GE(bytes_left, 0); + p += fragment_len; + frag_num++; + if (bytes_left <= tests[i].cruft_len) { + CHECK_EQ(status, GZipHeader::COMPLETE_HEADER); + break; + } else { + CHECK_EQ(status, GZipHeader::INCOMPLETE_HEADER); + } + } // while + } // for many fragmentations + } // for all test case headers +} + +} // namespace firebase diff --git a/app/rest/tests/request_binary_test.cc b/app/rest/tests/request_binary_test.cc new file mode 100644 index 0000000000..2d99bb6581 --- /dev/null +++ b/app/rest/tests/request_binary_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Google LLC + * + * 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 +#include + +#include "app/rest/request_binary.h" +#include "app/rest/request_binary_gzip.h" +#include "app/rest/request_options.h" +#include "app/rest/zlibwrapper.h" +#include "app/rest/tests/request_test.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +class RequestBinaryTest : public ::testing::Test { + protected: + // Codec that decompresses a gzip encoded string. + static std::string Decompress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_length = zlib.GzipUncompressedLength( + reinterpret_cast(input.data()), input.length()); + std::unique_ptr result(new char[result_length]); + int err = zlib.Uncompress( + reinterpret_cast(result.get()), &result_length, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_length); + } +}; + +TEST_F(RequestBinaryTest, GetSmallPostFields) { + TestCreateAndReadRequestBody(kSmallString, + sizeof(kSmallString)); +} + +TEST_F(RequestBinaryTest, GetLargePostFields) { + std::string large_buffer = CreateLargeTextData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +TEST_F(RequestBinaryTest, GetSmallBinaryPostFields) { + TestCreateAndReadRequestBody(kSmallBinary, + sizeof(kSmallBinary)); +} + +TEST_F(RequestBinaryTest, GetLargeBinaryPostFields) { + std::string large_buffer = CreateLargeBinaryData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +TEST_F(RequestBinaryTest, GetSmallPostFieldsWithGzip) { + TestCreateAndReadRequestBody( + kSmallString, sizeof(kSmallString), Decompress); +} + +TEST_F(RequestBinaryTest, GetLargePostFieldsWithGzip) { + std::string large_buffer = CreateLargeTextData(); + TestCreateAndReadRequestBody( + large_buffer.c_str(), large_buffer.size(), Decompress); +} + +TEST_F(RequestBinaryTest, GetSmallBinaryPostFieldsWithGzip) { + TestCreateAndReadRequestBody( + kSmallBinary, sizeof(kSmallBinary), Decompress); +} + +TEST_F(RequestBinaryTest, GetLargeBinaryPostFieldsWithGzip) { + std::string large_buffer = CreateLargeBinaryData(); + TestCreateAndReadRequestBody( + large_buffer.c_str(), large_buffer.size(), Decompress); +} + +} // namespace test +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_file_test.cc b/app/rest/tests/request_file_test.cc new file mode 100644 index 0000000000..64ceb3f2bb --- /dev/null +++ b/app/rest/tests/request_file_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2018 Google LLC + * + * 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 +#include + +#include "app/rest/request_file.h" +#include "app/rest/tests/request_test.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +class RequestFileTest : public ::testing::Test { + public: + RequestFileTest() + : filename_(FLAGS_test_tmpdir + "/a_file.txt"), + file_(nullptr), + file_size_(0) {} + + void SetUp() override; + void TearDown() override; + + protected: + std::string filename_; + FILE* file_; + size_t file_size_; + + static const char kFileContents[]; +}; + +const char RequestFileTest::kFileContents[] = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " + "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim " + "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "aliquip ex ea commodo consequat. Duis aute irure dolor in " + "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " + "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."; + +void RequestFileTest::SetUp() { + file_size_ = sizeof(kFileContents) - 1; + file_ = fopen(filename_.c_str(), "wb"); + CHECK(file_ != nullptr); + CHECK_EQ(file_size_, fwrite(kFileContents, 1, file_size_, file_)); + CHECK_EQ(0, fclose(file_)); +} + +void RequestFileTest::TearDown() { CHECK_EQ(0, unlink(filename_.c_str())); } + +TEST_F(RequestFileTest, NonExistentFile) { + RequestFile request("a_file_that_doesnt_exist.txt", 0); + EXPECT_FALSE(request.IsFileOpen()); +} + +TEST_F(RequestFileTest, OpenFile) { + RequestFile request(filename_.c_str(), 0); + EXPECT_TRUE(request.IsFileOpen()); +} + +TEST_F(RequestFileTest, GetFileSize) { + RequestFile request(filename_.c_str(), 0); + EXPECT_EQ(file_size_, request.file_size()); +} + +TEST_F(RequestFileTest, ReadFile) { + RequestFile request(filename_.c_str(), 0); + EXPECT_EQ(kFileContents, ReadRequestBody(&request)); +} + +TEST_F(RequestFileTest, ReadFileFromOffset) { + size_t read_offset = 29; + RequestFile request(filename_.c_str(), read_offset); + EXPECT_EQ(&kFileContents[read_offset], ReadRequestBody(&request)); +} + +} // namespace test +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_json_test.cc b/app/rest/tests/request_json_test.cc new file mode 100644 index 0000000000..1875c1447e --- /dev/null +++ b/app/rest/tests/request_json_test.cc @@ -0,0 +1,71 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/request_json.h" +#include "app/rest/sample_generated.h" +#include "app/rest/sample_resource.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class RequestSample : public RequestJson { + public: + RequestSample() : RequestJson(sample_resource_data) {} + + void set_token(const char* token) { + application_data_->token = token; + UpdatePostFields(); + } + + void set_number(int number) { + application_data_->number = number; + UpdatePostFields(); + } + + void UpdatePostFieldForTest() { + UpdatePostFields(); + } +}; + +// Test the creation. +TEST(RequestJsonTest, Creation) { + RequestSample request; + EXPECT_TRUE(request.options().post_fields.empty()); +} + +// Test the case where no field is set. +TEST(RequestJsonTest, UpdatePostFieldsEmpty) { + RequestSample request; + request.UpdatePostFieldForTest(); + EXPECT_EQ("{\n" + "}\n", request.options().post_fields); +} + +// Test with fields set. +TEST(RequestJsonTest, UpdatePostFields) { + RequestSample request; + request.set_number(123); + request.set_token("abc"); + EXPECT_EQ("{\n" + " token: \"abc\",\n" + " number: 123\n" + "}\n", request.options().post_fields); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_test.cc b/app/rest/tests/request_test.cc new file mode 100644 index 0000000000..2fcd2238bf --- /dev/null +++ b/app/rest/tests/request_test.cc @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/request.h" + +#include "app/rest/tests/request_test.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +TEST(RequestTest, SetUrl) { + Request request; + EXPECT_EQ("", request.options().url); + + request.set_url("some.url"); + EXPECT_EQ("some.url", request.options().url); +} + +TEST(RequestTest, GetSmallPostFields) { + TestCreateAndReadRequestBody(kSmallString, sizeof(kSmallString)); +} + +TEST(RequestTest, GetLargePostFields) { + std::string large_buffer = CreateLargeTextData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +TEST(RequestTest, GetSmallBinaryPostFields) { + TestCreateAndReadRequestBody(kSmallBinary, sizeof(kSmallBinary)); +} + +TEST(RequestTest, GetLargeBinaryPostFields) { + std::string large_buffer = CreateLargeBinaryData(); + TestCreateAndReadRequestBody(large_buffer.c_str(), + large_buffer.size()); +} + +} // namespace test +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/request_test.h b/app/rest/tests/request_test.h new file mode 100644 index 0000000000..e2b0b87669 --- /dev/null +++ b/app/rest/tests/request_test.h @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +#ifndef FIREBASE_APP_CLIENT_CPP_REST_TESTS_REQUEST_TEST_H_ +#define FIREBASE_APP_CLIENT_CPP_REST_TESTS_REQUEST_TEST_H_ + +#include +#include +#include +#include + +#include "app/rest/request.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace test { + +const char kSmallString[] = "hello world"; +const char kSmallBinary[] = {'a', 'b', '\0', 'c', '\0', 'x', 'y', 'z'}; +const size_t kLargeDataSize = 10 * 1024 * 1024; + +// Read data from a request into a string. +static std::string ReadRequestBody(Request* request) { + std::string output; + EXPECT_TRUE(request->ReadBodyIntoString(&output)); + return output; +} + +// No-op codec method that returns the specified string. +static std::string NoCodec(const std::string& string_to_decode) { + return string_to_decode; +} + +// Test creating and reading from a request. +template +void TestCreateAndReadRequestBody( + const char* buffer, size_t size, + std::function codec = NoCodec) { + { + // Test read without copying into the request. + std::vector modified_expected(buffer, buffer + size); + std::vector copy(modified_expected); + T request(©[0], copy.size()); + // Modify the buffer to validate it wasn't copied by the request. + for (size_t i = 0; i < size; ++i) { + copy[i]++; + modified_expected[i]++; + } + EXPECT_EQ(std::string(&modified_expected[0], size), + codec(ReadRequestBody(&request))); + } + { + const std::string expected(buffer, size); + T request; + { + // This allocates the string on the heap to ensure the memory is stomped + // with a pattern when deallocated in debug mode. + // Same below. + std::unique_ptr copy(new std::string(expected)); + request.set_post_fields(copy->c_str(), copy->length()); + } + EXPECT_EQ(expected, codec(ReadRequestBody(&request))); + } + { + const std::string expected(buffer); + T request; + { + std::unique_ptr copy(new std::string(expected)); + request.set_post_fields(copy->c_str()); + } + EXPECT_EQ(expected, codec(ReadRequestBody(&request))); + } +} + +// Create a random data stream of characters 0-9. +static const std::string CreateLargeTextData() { + std::string s; + unsigned int seed = 0; + srand(seed); + for (size_t i = 0; i < kLargeDataSize; i++) { + s += '0' + (rand() % 10); // NOLINT (rand_r() doesn't work on MSVC) + } + return s; +} + +// Create a random stream of binary data. +static const std::string CreateLargeBinaryData() { + std::string s; + unsigned int seed = 0; + srand(seed); + for (size_t i = 0; i < kLargeDataSize; i++) { + s += static_cast(rand()); // NOLINT (rand_r() doesn't work on MSVC) + } + return s; +} + +} // namespace test +} // namespace rest +} // namespace firebase + +#endif // FIREBASE_APP_CLIENT_CPP_REST_TESTS_REQUEST_TEST_H_ diff --git a/app/rest/tests/response_binary_test.cc b/app/rest/tests/response_binary_test.cc new file mode 100644 index 0000000000..8ec8e72bc1 --- /dev/null +++ b/app/rest/tests/response_binary_test.cc @@ -0,0 +1,128 @@ +/* + * Copyright 2017 Google LLC + * + * 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 + +#include "app/rest/response_binary.h" +#include "app/rest/zlibwrapper.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class ResponseBinaryTest : public ::testing::Test { + protected: + std::string Compress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_size = ZLib::MinCompressbufSize(input.length()); + std::unique_ptr result(new char[result_size]); + int err = zlib.Compress( + reinterpret_cast(result.get()), &result_size, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_size); + } + + std::string GetBody() { + const char* data; + size_t size; + response_.GetBody(&data, &size); + return std::string(data, size); + } + + void SetBody(const std::string& body) { + response_.ProcessBody(body.data(), body.length()); + response_.MarkCompleted(); + } + + ResponseBinary response_; +}; + +TEST_F(ResponseBinaryTest, GetBodyWihoutGunzip) { + std::string s = "hello world"; + SetBody(s); + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBinaryBodyWihoutGunzip) { + char buffer[] = {'a', 'b', '\0', 'c', '\0', 'x', 'y', 'z'}; + std::string s(buffer, sizeof(buffer)); + SetBody(s); + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBodyWihGunzip) { + response_.set_use_gunzip(true); + + std::string s = "hello world"; + SetBody(Compress(s)); + + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBinaryBodyWihGunzip) { + response_.set_use_gunzip(true); + + char buffer[] = {'a', 'b', '\0', 'c', '\0', 'x', 'y', 'z'}; + std::string s(buffer, sizeof(buffer)); + SetBody(Compress(s)); + + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBodyWihGunzipHugeBuffer) { + response_.set_use_gunzip(true); + + // 10 MB body + std::string s; + unsigned int seed = 0; + srand(seed); + size_t size = 10 * 1024 * 1024; + for (size_t i = 0; i < size; i++) { + s += '0' + (rand() % 10); // NOLINT (rand_r() doesn't work on windows) + } + SetBody(Compress(s)); + + EXPECT_EQ(GetBody(), s); +} + +TEST_F(ResponseBinaryTest, GetBinaryBodyWihGunzipHugeBuffer) { + response_.set_use_gunzip(true); + + // 10 MB body + size_t size = 10 * 1024 * 1024; + char* buffer = new char[size]; + + unsigned int seed = 0; + srand(seed); + for (size_t i = 0; i < size; i++) { + // Add 0-9 numbers and '\0' to buffer. + buffer[i] = (i % 10) ? ('0' + (rand() % 10)): '\0'; // NOLINT + // (no rand_r on msvc) + } + + std::string s(buffer, sizeof(buffer)); + SetBody(Compress(s)); + EXPECT_EQ(GetBody(), s); + + delete[] buffer; +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/response_json_test.cc b/app/rest/tests/response_json_test.cc new file mode 100644 index 0000000000..0f2f94f3f7 --- /dev/null +++ b/app/rest/tests/response_json_test.cc @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/response_json.h" +#include +#include "app/rest/sample_generated.h" +#include "app/rest/sample_resource.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class ResponseSample : public ResponseJson { + public: + ResponseSample() : ResponseJson(sample_resource_data) {} + ResponseSample(ResponseSample&& rhs) : ResponseJson(std::move(rhs)) {} + + std::string token() const { + return application_data_ ? application_data_->token : std::string(); + } + + int number() const { + return application_data_ ? application_data_->number : 0; + } +}; + +// Test the creation. +TEST(ResponseJsonTest, Creation) { + ResponseSample response; + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); +} + +// Test move operation. +TEST(ResponseJsonTest, Move) { + ResponseSample src; + const char body[] = + "{" + " \"token\": \"abc\"," + " \"number\": 123" + "}"; + src.ProcessBody(body, sizeof(body)); + src.MarkCompleted(); + const auto check_non_empty = [](const ResponseSample& response) { + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_EQ("abc", response.token()); + EXPECT_EQ(123, response.number()); + }; + check_non_empty(src); + + ResponseSample dest = std::move(src); + // src should now be moved-from and its parsed fields should be blank. + // NOLINT disables ClangTidy checks that warn about access to moved-from + // object. In this case, this is deliberate. The only data member that gets + // accessed is application_data_, which is std::unique_ptr and has + // well-defined state (equivalent to default-created). + EXPECT_TRUE(src.token().empty()); // NOLINT + EXPECT_EQ(0, src.number()); // NOLINT + // dest should now contain everything src contained. + check_non_empty(dest); +} + +// Test the case server respond with just {}. +TEST(ResponseJsonTest, EmptyJsonResponse) { + ResponseSample response; + const char body[] = + "{" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_TRUE(response.token().empty()); + EXPECT_EQ(0, response.number()); +} + +// Test the case server respond with non-empty standard JSON string. +TEST(ResponseJsonTest, StandardJsonResponse) { + ResponseSample response; + const char body[] = + "{" + " \"token\": \"abc\"," + " \"number\": 123" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_EQ("abc", response.token()); + EXPECT_EQ(123, response.number()); +} + +// Test the case server respond with non-empty JSON string. +TEST(ResponseJsonTest, NonStandardJsonResponse) { + ResponseSample response; + // JSON format has non-standard variations: + // quotation around field name or not; + // quotation around non-string field value or not; + // single quotes vs double quotes + // Here we try some of the non-standard variations. + const char body[] = + "{" + " token: 'abc'," + " 'number': '123'" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_EQ("abc", response.token()); + EXPECT_EQ(123, response.number()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/response_test.cc b/app/rest/tests/response_test.cc new file mode 100644 index 0000000000..00adb7d159 --- /dev/null +++ b/app/rest/tests/response_test.cc @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/response.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +// A helper function that prepares char buffer and calls the response's +// ProcessHeader. Param str must be a C string. +void ProcessHeader(const char* str, Response* response) { + // Prepare the char buffer to call ProcessHeader and make sure the + // implementation does not rely on that buffer is \0 terminated. + size_t length = strlen(str); + char* buffer = new char[length + 20]; // We pad the buffer with a few '#'. + memset(buffer, '#', length + 20); + memcpy(buffer, str, length); // Intentionally not copy the \0. + + // Now call the ProcessHeader. + response->ProcessHeader(buffer, length); + delete[] buffer; +} + +TEST(ResponseTest, ProcessStatusLine) { + Response response; + EXPECT_EQ(0, response.status()); + + ProcessHeader("HTTP/1.1 200 OK\r\n", &response); + EXPECT_EQ(200, response.status()); + + ProcessHeader("HTTP/1.1 302 Found\r\n", &response); + EXPECT_EQ(302, response.status()); +} + +TEST(ResponseTest, ProcessHeaderEnding) { + Response response; + EXPECT_FALSE(response.header_completed()); + + ProcessHeader("HTTP/1.1 200 OK\r\n", &response); + EXPECT_FALSE(response.header_completed()); + + ProcessHeader("\r\n", &response); + EXPECT_TRUE(response.header_completed()); +} + +TEST(ResponseTest, ProcessHeaderField) { + Response response; + EXPECT_STREQ(nullptr, response.GetHeader("Content-Type")); + EXPECT_STREQ(nullptr, response.GetHeader("Date")); + EXPECT_STREQ(nullptr, response.GetHeader("key")); + + ProcessHeader("Content-Type: text/html; charset=UTF-8\r\n", &response); + ProcessHeader("Date: Wed, 05 Jul 2017 15:55:19 GMT\r\n", &response); + ProcessHeader("key: value\r\n", &response); + EXPECT_STREQ("text/html; charset=UTF-8", response.GetHeader("Content-Type")); + EXPECT_STREQ("Wed, 05 Jul 2017 15:55:19 GMT", response.GetHeader("Date")); + EXPECT_STREQ("value", response.GetHeader("key")); +} + +// Below test the fetch-time logic for various test cases. +TEST(ResponseTest, ProcessDateHeaderValidDate) { + Response response; + EXPECT_EQ(0, response.fetch_time()); + ProcessHeader("Date: Wed, 05 Jul 2017 15:55:19 GMT\r\n", &response); + response.MarkCompleted(); + EXPECT_EQ(1499270119, response.fetch_time()); +} + +TEST(ResponseTest, ProcessDateHeaderInvalidDate) { + Response response; + EXPECT_EQ(0, response.fetch_time()); + ProcessHeader("Date: here is a invalid date\r\n", &response); + response.MarkCompleted(); + EXPECT_LT(1499270119, response.fetch_time()); +} + +TEST(ResponseTest, ProcessDateHeaderMissing) { + Response response; + EXPECT_EQ(0, response.fetch_time()); + response.MarkCompleted(); + EXPECT_LT(1499270119, response.fetch_time()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/testdata/sample.fbs b/app/rest/tests/testdata/sample.fbs new file mode 100644 index 0000000000..332255f22b --- /dev/null +++ b/app/rest/tests/testdata/sample.fbs @@ -0,0 +1,24 @@ +// Copyright 2017 Google LLC +// +// 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. + +// A simple FlatBuffer schema as a sample. + +namespace firebase.rest; + +table Sample { + token:string; + number:int; +} + +root_type Sample; diff --git a/app/rest/tests/transport_curl_test.cc b/app/rest/tests/transport_curl_test.cc new file mode 100644 index 0000000000..b484f14765 --- /dev/null +++ b/app/rest/tests/transport_curl_test.cc @@ -0,0 +1,180 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +// This is a large test that starts a local http server and tests transport_curl +// with actual http connection. + +#include "app/rest/transport_curl.h" + +#include +#include + +#include "app/rest/request.h" +#include "app/rest/response.h" +#include "net/http2/server/lib/public/httpserver2.h" +#include "net/util/ports.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "absl/strings/str_format.h" +#include "absl/synchronization/mutex.h" +#include "absl/time/time.h" +#include "util/task/status.h" + +namespace firebase { +namespace rest { + +const char* kServerVersion = "HTTP server for test"; + +void UriHandler(HTTPServerRequest* request) { + if (request->http_method() == "GET") { + request->output()->WriteString("test"); + request->Reply(); + LOG(INFO) << "Sent response for GET"; + } else if (request->http_method() == "POST" && + request->input_headers()->HeaderIs("Content-Type", + "application/json")) { + request->output()->WriteString(request->input()->ToString()); + request->Reply(); + LOG(INFO) << "Sent response for POST"; + } else { + FAIL(); + } +} + +const absl::Duration kTimeoutSeconds = absl::Seconds(10); + +class TestResponse : public Response { + public: + void MarkCompleted() override { + absl::MutexLock lock(&mutex_); + Response::MarkCompleted(); + } + + void Wait() { + absl::MutexLock lock(&mutex_); + mutex_.AwaitWithTimeout( + absl::Condition( + [](void* userdata) -> bool { + auto* response = static_cast(userdata); + return response->header_completed() && response->body_completed(); + }, + this), + kTimeoutSeconds); + } + + private: + absl::Mutex mutex_; +}; + +class TransportCurlTest : public testing::Test { + protected: + static void SetUpTestSuite() { + InitTransportCurl(); + // Start a local http server for testing the http request. + // Pick up a port. + std::string error; // PickUnusedPort actually asks for google3 string. + TransportCurlTest::port_ = net_util::PickUnusedPort(&error); + CHECK_GE(TransportCurlTest::port_, 0) << error; + LOG(INFO) << "Auto selected port " << port_ << " for test http server"; + // Create a new server. + std::unique_ptr options( + new net_http2::HTTPServer2::EventModeOptions()); + options->SetVersion(kServerVersion); + options->SetDataVersion("data_1.0"); + options->SetServerType("server"); + options->AddPort(TransportCurlTest::port_); + options->SetWindowSizesAndLatency(0, 0, true); + auto creation_status = net_http2::HTTPServer2::CreateEventDrivenModeServer( + nullptr /* event manager */, std::move(options)); + CHECK_OK(creation_status.status()); + // Register URI handler and start serving. + TransportCurlTest::server_ = creation_status.value().release(); + ABSL_DIE_IF_NULL(TransportCurlTest::server_) + ->RegisterHandler("*", NewPermanentCallback(&UriHandler)); + CHECK_OK(TransportCurlTest::server_->StartAcceptingRequests()); + LOG(INFO) << "Local HTTP server is ready to accept request"; + } + static void TearDownTestSuite() { + TransportCurlTest::server_->TerminateServer(); + delete TransportCurlTest::server_; + TransportCurlTest::server_ = nullptr; + CleanupTransportCurl(); + } + static int32 port_; + static net_http2::HTTPServer2* server_; +}; + +int32 TransportCurlTest::port_; +net_http2::HTTPServer2* TransportCurlTest::server_; + +TEST_F(TransportCurlTest, TestGlobalInitAndCleanup) { + InitTransportCurl(); + CleanupTransportCurl(); +} + +TEST_F(TransportCurlTest, TestCreation) { TransportCurl curl; } + +TEST_F(TransportCurlTest, TestHttpGet) { + Request request; + request.set_verbose(true); + TestResponse response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + EXPECT_EQ(nullptr, response.GetHeader("Server")); + EXPECT_STREQ("", response.GetBody()); + + const std::string& url = + absl::StrFormat("http://localhost:%d", TransportCurlTest::port_); + request.set_url(url.c_str()); + TransportCurl curl; + curl.Perform(request, &response); + response.Wait(); + EXPECT_EQ(200, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_STREQ(kServerVersion, response.GetHeader("Server")); + EXPECT_STREQ("test", response.GetBody()); +} + +TEST_F(TransportCurlTest, TestHttpPost) { + Request request; + request.set_verbose(true); + TestResponse response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + EXPECT_EQ(nullptr, response.GetHeader("Server")); + EXPECT_STREQ("", response.GetBody()); + + const std::string& url = + absl::StrFormat("http://localhost:%d", TransportCurlTest::port_); + request.set_url(url.c_str()); + request.set_method("POST"); + request.add_header("Content-Type", "application/json"); + request.set_post_fields("{'a':'a','b':'b'}"); + TransportCurl curl; + curl.Perform(request, &response); + response.Wait(); + EXPECT_EQ(200, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_STREQ(kServerVersion, response.GetHeader("Server")); + EXPECT_STREQ("{'a':'a','b':'b'}", response.GetBody()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/transport_mock_test.cc b/app/rest/tests/transport_mock_test.cc new file mode 100644 index 0000000000..ea759dd33b --- /dev/null +++ b/app/rest/tests/transport_mock_test.cc @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/transport_mock.h" +#include "app/rest/request.h" +#include "app/rest/response.h" +#include "testing/config.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +TEST(TransportMockTest, TestCreation) { TransportMock mock; } + +TEST(TransportMockTest, TestHttpGet200) { + Request request; + Response response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + EXPECT_EQ(nullptr, response.GetHeader("Server")); + EXPECT_STREQ("", response.GetBody()); + + request.set_url("http://my.fake.site"); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'http://my.fake.site'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['this is a mock',]" + " }" + " }" + " ]" + "}"); + TransportMock transport; + transport.Perform(request, &response); + EXPECT_EQ(200, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); + EXPECT_STREQ("mock server 101", response.GetHeader("Server")); + EXPECT_STREQ("this is a mock", response.GetBody()); +} + +TEST(TransportMockTest, TestHttpGet404) { + Request request; + Response response; + EXPECT_EQ(0, response.status()); + EXPECT_FALSE(response.header_completed()); + EXPECT_FALSE(response.body_completed()); + + request.set_url("http://my.fake.site"); + firebase::testing::cppsdk::ConfigSet("{config:[]}"); + TransportMock transport; + transport.Perform(request, &response); + EXPECT_EQ(404, response.status()); + EXPECT_TRUE(response.header_completed()); + EXPECT_TRUE(response.body_completed()); +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/util_test.cc b/app/rest/tests/util_test.cc new file mode 100644 index 0000000000..a7f5cff05c --- /dev/null +++ b/app/rest/tests/util_test.cc @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/rest/util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { +namespace util { + +TEST(UtilTest, TestTrimWhitespace) { + // Empty + EXPECT_EQ("", TrimWhitespace("")); + // Only white space + EXPECT_EQ("", TrimWhitespace(" ")); + EXPECT_EQ("", TrimWhitespace(" \r\n \t ")); + // A single letter + EXPECT_EQ("x", TrimWhitespace(" x")); + EXPECT_EQ("x", TrimWhitespace("x ")); + EXPECT_EQ("x", TrimWhitespace(" x ")); + // A word + EXPECT_EQ("abc", TrimWhitespace("\t abc")); + EXPECT_EQ("abc", TrimWhitespace("abc \r\n")); + EXPECT_EQ("abc", TrimWhitespace("\t abc \r\n")); + // A few words + EXPECT_EQ("mary had little lamb", TrimWhitespace(" mary had little lamb")); + EXPECT_EQ("mary had little lamb", TrimWhitespace("mary had little lamb ")); + EXPECT_EQ("mary had little lamb", TrimWhitespace(" mary had little lamb ")); +} + +TEST(UtilTest, TestToUpper) { + // Empty + EXPECT_EQ("", ToUpper("")); + // Only non-alpha characters + EXPECT_EQ("3.1415926", ToUpper("3.1415926")); + // Letters + EXPECT_EQ("A", ToUpper("a")); + EXPECT_EQ("ABC", ToUpper("AbC")); + // Mixed + EXPECT_EQ("789 ABC", ToUpper("789 abc")); + EXPECT_EQ("1A2B3C", ToUpper("1a2b3c")); +} + +} // namespace util +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/www_form_url_encoded_test.cc b/app/rest/tests/www_form_url_encoded_test.cc new file mode 100644 index 0000000000..6aaa6ceb3b --- /dev/null +++ b/app/rest/tests/www_form_url_encoded_test.cc @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/rest/www_form_url_encoded.h" + +#include "app/rest/util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace rest { + +class WwwFormUrlEncodedTest : public ::testing::Test { + protected: + void SetUp() override { util::Initialize(); } + + void TearDown() override { util::Terminate(); } +}; + +TEST_F(WwwFormUrlEncodedTest, Initialize) { + std::string initial("something"); + WwwFormUrlEncoded form(&initial); + EXPECT_EQ(initial, form.form_data()); +} + +TEST_F(WwwFormUrlEncodedTest, AddFields) { + std::string form_data; + WwwFormUrlEncoded form(&form_data); + form.Add("foo", "bar"); + form.Add("bash", "bish bosh"); + form.Add("h:&=l\nlo", "g@@db=\r\tye&\xfe"); + form.Add(WwwFormUrlEncoded::Item("hip", "hop")); + EXPECT_EQ("foo=bar&bash=bish%20bosh&" + "h%3A%26%3Dl%0Alo=g%40%40db%3D%0D%09ye%26%FE&" + "hip=hop", + form.form_data()); +} + +TEST_F(WwwFormUrlEncodedTest, ParseEmpty) { + auto items = WwwFormUrlEncoded::Parse(""); + EXPECT_EQ(0, items.size()); +} + +TEST_F(WwwFormUrlEncodedTest, ParseForm) { + WwwFormUrlEncoded::Item expected_items[] = { + WwwFormUrlEncoded::Item("h:llo", "g@@dbye&"), + WwwFormUrlEncoded::Item("bash", "bish bosh"), + }; + auto items = WwwFormUrlEncoded::Parse( + "h%3Allo=g%40%40dbye%26&" + "bash=bish%20bosh"); + EXPECT_EQ(sizeof(expected_items) / sizeof(expected_items[0]), items.size()); + for (size_t i = 0; i < items.size(); ++i) { + EXPECT_EQ(expected_items[i].key, items[i].key) << "Key " << i; + EXPECT_EQ(expected_items[i].value, items[i].value) << "Value " << i; + } +} + +TEST_F(WwwFormUrlEncodedTest, ParseFormWithOtherSeparators) { + WwwFormUrlEncoded::Item expected_items[] = { + WwwFormUrlEncoded::Item("h:llo", "g@@dbye&"), + WwwFormUrlEncoded::Item("bash", "bish bosh"), + WwwFormUrlEncoded::Item("hello", "you"), + }; + auto items = WwwFormUrlEncoded::Parse( + "h%3Allo=g%40%40dbye%26&\r " + "bash=bish%20bosh\n&\t&\nhello=you"); + EXPECT_EQ(sizeof(expected_items) / sizeof(expected_items[0]), items.size()); + for (size_t i = 0; i < items.size(); ++i) { + EXPECT_EQ(expected_items[i].key, items[i].key) << "Key " << i; + EXPECT_EQ(expected_items[i].value, items[i].value) << "Value " << i; + } +} + +TEST_F(WwwFormUrlEncodedTest, ParseFormWithInvalidFields) { + WwwFormUrlEncoded::Item expected_items[] = { + WwwFormUrlEncoded::Item("h:llo", "g@@dbye&"), + WwwFormUrlEncoded::Item("bash", "bish bosh"), + }; + auto items = WwwFormUrlEncoded::Parse( + "h%3Allo=g%40%40dbye%26&" + "invalidfield0&" + "bash=bish%20bosh&" + "moreinvaliddata&" + "ignorethisaswell"); + EXPECT_EQ(sizeof(expected_items) / sizeof(expected_items[0]), items.size()); + for (size_t i = 0; i < items.size(); ++i) { + EXPECT_EQ(expected_items[i].key, items[i].key) << "Key " << i; + EXPECT_EQ(expected_items[i].value, items[i].value) << "Value " << i; + } +} + +} // namespace rest +} // namespace firebase diff --git a/app/rest/tests/zlibwrapper_unittest.cc b/app/rest/tests/zlibwrapper_unittest.cc new file mode 100644 index 0000000000..9d8e31d62f --- /dev/null +++ b/app/rest/tests/zlibwrapper_unittest.cc @@ -0,0 +1,1050 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/rest/zlibwrapper.h" + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "absl/base/macros.h" +#include "absl/strings/escaping.h" +#include "util/random/acmrandom.h" + +// 1048576 == 2^20 == 1 MB +#define MAX_BUF_SIZE 1048500 +#define MAX_BUF_FLEX 1048576 + +DEFINE_int32(min_comp_lvl, 6, "Minimum compression level"); +DEFINE_int32(max_comp_lvl, 6, "Maximum compression level"); +DEFINE_string(dict, "", "Dictionary file to use (overrides default text)"); +DEFINE_string(files_to_process, "", + "Comma separated list of filenames to read in for our tests. " + "If empty, a default file from testdata is used."); +DEFINE_int32(zlib_max_size_uncompressed_data, 10 * 1024 * 1024, // 10MB + "Maximum expected size of the uncompress length " + "in the gzip footer."); +DEFINE_string(read_past_window_data_file, "", + "Data to use for reproducing read-past-window bug;" + " defaults to zlib/testdata/read_past_window.data"); +DEFINE_int32(read_past_window_iterations, 4000, + "Number of attempts to read past end of window"); +ABSL_FLAG(absl::Duration, slow_test_deadline, absl::Minutes(2), + "The voluntary time limit some of the slow tests attempt to " + "adhere to. Used only if the build is detected as an unusually " + "slow one according to ValgrindSlowdown(). Set to \"inf\" to " + "disable."); + +namespace firebase { + +namespace { + +// A helper class for build configurations that really slow down the build. +// +// Some of this file's tests are so CPU intensive that they no longer +// finish in a reasonable time under "sanitizer" builds. These builds +// advertise themselves with a ValgrindSlowdown() > 1.0. Use this class to +// abandon tests after reasonable deadlines. +class SlowTestLimiter { + public: + // Initializes the deadline relative to absl::Now(). + SlowTestLimiter(); + + // A human readable reason for the limiter's policy. + std::string reason() { return reason_; } + + // Returns true if this known to be a slow build. + bool IsSlowBuild() const { return deadline_ < absl::InfiniteFuture(); } + + // Returns true iff absl::Now() > deadline(). This class is passive; the + // test must poll. + bool DeadlineExceeded() const { return absl::Now() > deadline_; } + + private: + std::string reason_; + absl::Time deadline_; +}; + +SlowTestLimiter::SlowTestLimiter() { + deadline_ = absl::InfiniteFuture(); + double slowdown = ValgrindSlowdown(); + reason_ = + absl::StrCat("ValgrindSlowdown() of ", absl::LegacyPrecision(slowdown)); + if (slowdown <= 1.0) return; + absl::Duration relative_deadline = absl::GetFlag(FLAGS_slow_test_deadline); + absl::StrAppend(&reason_, " with --slow_test_deadline=", + absl::FormatDuration(relative_deadline)); + deadline_ = absl::Now() + relative_deadline; +} + +REGISTER_MODULE_INITIALIZER(zlibwrapper_unittest, { + SlowTestLimiter limiter; + LOG(WARNING) + << "SlowTestLimiter policy " + << (limiter.IsSlowBuild() + ? "limited; slow tests will voluntarily limit execution time." + : "unlimited.") + << " Reason: " << limiter.reason(); +}); + +bool ReadFileToString(const std::string& filename, std::string* output, + int64 max_size) { + std::ifstream f; + f.open(filename); + if (f.fail()) { + return false; + } + f.seekg(0, std::ios::end); + int64 length = std::min(static_cast(f.tellg()), max_size); + f.seekg(0, std::ios::beg); + output->resize(length); + f.read(&*output->begin(), length); + f.close(); + return !f.fail(); +} + +void TestCompression(ZLib* zlib, const std::string& uncompbuf, + const char* msg) { + LOG(INFO) << "TestCompression of " << uncompbuf.size() << " bytes."; + + uLongf complen = ZLib::MinCompressbufSize(uncompbuf.size()); + std::string compbuf(complen, '\0'); + int err = zlib->Compress((Bytef*)compbuf.data(), &complen, + (Bytef*)uncompbuf.data(), uncompbuf.size()); + EXPECT_EQ(Z_OK, err) << " " << uncompbuf.size() << " bytes down to " + << complen << " bytes."; + + // Output data size should match input data size. + uLongf uncomplen2 = uncompbuf.size(); + std::string uncompbuf2(uncomplen2, '\0'); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + + if (msg != nullptr) { + printf("Orig: %7lu Compressed: %7lu %5.3f %s\n", uncomplen2, complen, + (float)complen / uncomplen2, msg); + } + + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; +} + +// Due to a bug in old versions of zlibwrapper, we appended the gzip +// footer even in non-gzip mode. This tests that we can correctly +// uncompress this buggily-compressed data. +void TestBuggyCompression(ZLib* zlib, const std::string& uncompbuf) { + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + uLongf complen = compbuf.size(); + int err = zlib->Compress((Bytef*)compbuf.data(), &complen, + (Bytef*)uncompbuf.data(), uncompbuf.size()); + EXPECT_EQ(Z_OK, err) << " " << uncompbuf.size() << " bytes down to " + << complen << " bytes."; + + complen += 8; // 8 bytes is size of gzip footer + + uLongf uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // Make sure uncompress-chunk works as well + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + ASSERT_TRUE(zlib->UncompressChunkDone()); + + // Try to uncompress an incomplete chunk (missing 4 bytes from the + // gzip header, which we're ignoring). + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen - 4); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncompbuf, absl::string_view(uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // Repeat UncompressChunk with the rest of the gzip header. + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data() + complen - 4, 4); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(0, uncomplen2); + + ASSERT_TRUE(zlib->UncompressChunkDone()); + + // Uncompress works on a complete input, so it should be able to + // assume that either the gzip footer is all there or its not there at all. + // Make sure it doesn't work on things that don't look like gzip footers. + complen -= 4; // now we're smaller than the footer size + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_DATA_ERROR, err); + + complen += 8; // now we're bigger than the footer size + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_DATA_ERROR, err); +} + +// Make sure we uncompress right even if the first chunk is in the middle +// of the gzip headers, or in the middle of the gzip footers. (TODO) +void TestGzipHeaderUncompress(ZLib* zlib) { + struct { + const char* s; + int len; + int level; + } comp_chunks[][10] = { + // Level 0: no gzip footer (except partial footer for the last case) + // Level 1: normal gzip footer + // Level 2: extra byte after gzip footer + { + // divide up: header, body ("hello, world!\n"), footer + {"\037\213\010\000\216\176\356\075\002\003", 10, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266\016\000\000\000", 8, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: partial header, partial header, + // body ("hello, world!\n"), footer + {"\037\213\010\000\216", 5, 0}, + {"\176\356\075\002\003", 5, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266\016\000\000\000", 8, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: full header, + // body ("hello, world!\n"), partial footer, partial footer + {"\037\213\010\000\216\176\356\075\002\003", 10, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266", 4, 1}, + {"\016\000\000\000", 4, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: partial hdr, partial header, + // body ("hello, world!\n"), partial footer, partial footer + {"\037\213\010\000\216", 5, 0}, + {"\176\356\075\002\003", 5, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000", + 16, 0}, + {"\300\337\061\266", 4, 1}, + {"\016\000\000\000", 4, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }, + { + // divide up: partial hdr, partial header, + // body ("hello, world!\n") with partial footer, + // partial footer, partial footer + {"\037\213\010\000\216", 5, 0}, + {"\176\356\075\002\003", 5, 0}, + {"\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000" + // start of footer here. + "\300\337", + 18, 0}, + {"\061\266\016\000", 4, 1}, + {"\000\000", 2, 1}, + {"\n", 1, 2}, + {"", 0, 0}, + }}; + + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + for (int k = 0; k < 6; ++k) { + // k: < 3 with ZLib::should_be_flexible_with_gzip_footer_ true + // >= 3 with ZLib::should_be_flexible_with_gzip_footer_ false + // 0/3: no footer (partial footer for the last testing case) + // 1/4: normal footer + // 2/5: extra byte after footer + const int level = k % 3; + ZLib::set_should_be_flexible_with_gzip_footer(k < 3); + for (int j = 0; j < ABSL_ARRAYSIZE(comp_chunks); ++j) { + int bytes_uncompressed = 0; + zlib->Reset(); + int err = Z_OK; + for (int i = 0; comp_chunks[j][i].len != 0; ++i) { + if (comp_chunks[j][i].level <= level) { + uLongf uncomplen2 = uncompbuf2.size() - bytes_uncompressed; + err = zlib->UncompressChunk( + (Bytef*)&uncompbuf2[0] + bytes_uncompressed, &uncomplen2, + (const Bytef*)comp_chunks[j][i].s, comp_chunks[j][i].len); + if (err != Z_OK) { + LOG(INFO) << "err = " << err << " comp_chunks[" << j << "][" << i + << "] failed."; + break; + } else { + bytes_uncompressed += uncomplen2; + } + } + } + // With ZLib::should_be_flexible_with_gzip_footer_ being false, the no or + // partial footer (k == 3) and extra byte after footer (k == 5) cases + // should not work. With ZLib::should_be_flexible_with_gzip_footer_ being + // true, all cases should work. + if (k == 3 || k == 5) { + ASSERT_TRUE(err != Z_OK || !zlib->UncompressChunkDone()); + } else { + ASSERT_TRUE(zlib->UncompressChunkDone()); + LOG(INFO) << "Got " << bytes_uncompressed << " bytes: " + << absl::string_view(uncompbuf2.data(), bytes_uncompressed); + EXPECT_EQ(sizeof("hello, world!\n") - 1, bytes_uncompressed); + EXPECT_EQ(0, strncmp(uncompbuf2.data(), "hello, world!\n", + bytes_uncompressed)) + << "Uncompression mismatch, expected 'hello, world!\\n', " + << "got '" + << absl::string_view(uncompbuf2.data(), bytes_uncompressed) << "'"; + } + } + } +} + +// Take some test inputs and pass them to zlib, fragmenting the input +// in many different random ways. +void TestRandomGzipHeaderUncompress(ZLib* zlib) { + ACMRandom rnd(ACMRandom::DeprecatedDefaultSeed()); + + struct TestCase { + const char* str; + int len; // total length of the string + }; + TestCase tests[] = { + { + // header, body ("hello, world!\n"), footer + "\037\213\010\000\216\176\356\075\002\003" + "\313\110\315\311\311\327\121\050\317\057\312\111\121\344\002\000" + "\300\337\061\266\016\000\000\000", + 34, + }, + }; + + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + // Test all the headers test cases. + for (int i = 0; i < ABSL_ARRAYSIZE(tests); ++i) { + // Test many random ways they might be fragmented. + for (int j = 0; j < 5 * 1000; ++j) { + // Get the test case set up. + const char* p = tests[i].str; + int bytes_left = tests[i].len; + int bytes_read = 0; + int bytes_uncompressed = 0; + zlib->Reset(); + + // Pick some random places to fragment the headers. + const int num_fragments = rnd.Uniform(bytes_left); + std::vector fragment_starts; + for (int frag_num = 0; frag_num < num_fragments; ++frag_num) { + fragment_starts.push_back(rnd.Uniform(bytes_left)); + } + std::sort(fragment_starts.begin(), fragment_starts.end()); + + VLOG(1) << "====="; + + // Go through several fragments and pass them in for parsing. + int frag_num = 0; + while (bytes_left > 0) { + const int fragment_len = (frag_num < num_fragments) + ? (fragment_starts[frag_num] - bytes_read) + : (tests[i].len - bytes_read); + ASSERT_GE(fragment_len, 0); + if (fragment_len != 0) { // zlib doesn't like 0-length buffers + VLOG(1) << absl::StrFormat( + "Passing %2d bytes at %2d..%2d: %s", fragment_len, bytes_read, + bytes_read + fragment_len, + absl::CEscape(std::string(p, fragment_len))); + + uLongf uncomplen2 = uncompbuf2.size() - bytes_uncompressed; + int err = + zlib->UncompressChunk((Bytef*)&uncompbuf2[0] + bytes_uncompressed, + &uncomplen2, (const Bytef*)p, fragment_len); + ASSERT_EQ(err, Z_OK); + bytes_uncompressed += uncomplen2; + bytes_read += fragment_len; + bytes_left -= fragment_len; + ASSERT_GE(bytes_left, 0); + p += fragment_len; + } + frag_num++; + } // while bytes left to uncompress + + ASSERT_TRUE(zlib->UncompressChunkDone()); + VLOG(1) << "Got " << bytes_uncompressed << " bytes: " + << absl::string_view(uncompbuf2.data(), bytes_uncompressed); + EXPECT_EQ(sizeof("hello, world!\n") - 1, bytes_uncompressed); + EXPECT_EQ( + 0, strncmp(uncompbuf2.data(), "hello, world!\n", bytes_uncompressed)) + << "Uncompression mismatch, expected 'hello, world!\\n', " + << "got '" << absl::string_view(uncompbuf2.data(), bytes_uncompressed) + << "'"; + } // for many fragmentations + } // for all test case headers +} + +// Make sure we give the proper error codes when inputs aren't quite kosher +void TestErrors(ZLib* zlib, const std::string& uncompbuf_str) { + const char* uncompbuf = uncompbuf_str.data(); + const uLongf uncomplen = uncompbuf_str.size(); + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + int err; + + uLongf complen = 23; // don't give it enough space to compress + err = zlib->Compress((Bytef*)compbuf.data(), &complen, (Bytef*)uncompbuf, + uncomplen); + EXPECT_EQ(Z_BUF_ERROR, err); + + // OK, now sucessfully compress + complen = compbuf.size(); + err = zlib->Compress((Bytef*)compbuf.data(), &complen, (Bytef*)uncompbuf, + uncomplen); + EXPECT_EQ(Z_OK, err) << " " << uncomplen << " bytes down to " << complen + << " bytes."; + + uLongf uncomplen2 = 100; // not enough space to uncompress + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_BUF_ERROR, err); + + // Here we check what happens when we don't try to uncompress enough bytes + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), 23); + EXPECT_EQ(Z_BUF_ERROR, err); + + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), 23); + EXPECT_EQ(Z_OK, err); // it's ok if a single chunk is too small + if (err == Z_OK) { + EXPECT_FALSE(zlib->UncompressChunkDone()) + << "UncompresDone() was happy with its 3 bytes of compressed data"; + } + + const int changepos = 0; + const char oldval = compbuf[changepos]; // corrupt the input + compbuf[changepos]++; + uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_NE(Z_OK, err); + + compbuf[changepos] = oldval; + + // Make sure our memory-allocating uncompressor deals with problems gracefully + char* tmpbuf; + char tmp_compbuf[10] = "\255\255\255\255\255\255\255\255\255"; + uncomplen2 = FLAGS_zlib_max_size_uncompressed_data; + err = zlib->UncompressGzipAndAllocate( + (Bytef**)&tmpbuf, &uncomplen2, (Bytef*)tmp_compbuf, sizeof(tmp_compbuf)); + EXPECT_NE(Z_OK, err); + EXPECT_EQ(nullptr, tmpbuf); +} + +// Make sure that UncompressGzipAndAllocate returns a correct error +// when asked to uncompress data that isn't gzipped. +void TestBogusGunzipRequest(ZLib* zlib) { + const Bytef compbuf[] = "This is not compressed"; + const uLongf complen = sizeof(compbuf); + Bytef* uncompbuf; + uLongf uncomplen = 0; + int err = + zlib->UncompressGzipAndAllocate(&uncompbuf, &uncomplen, compbuf, complen); + EXPECT_EQ(Z_DATA_ERROR, err); +} + +void TestGzip(ZLib* zlib, const std::string& uncompbuf_str) { + const char* uncompbuf = uncompbuf_str.data(); + const uLongf uncomplen = uncompbuf_str.size(); + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + + uLongf complen = compbuf.size(); + int err = zlib->Compress((Bytef*)compbuf.data(), &complen, (Bytef*)uncompbuf, + uncomplen); + EXPECT_EQ(Z_OK, err) << " " << uncomplen << " bytes down to " << complen + << " bytes."; + + uLongf uncomplen2 = uncompbuf2.size(); + err = zlib->Uncompress((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncomplen, uncomplen2) << "Uncompression mismatch!"; + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen)) + << "Uncompression mismatch!"; + + // Also try the auto-allocate uncompressor + char* tmpbuf; + err = zlib->UncompressGzipAndAllocate((Bytef**)&tmpbuf, &uncomplen2, + (Bytef*)compbuf.data(), complen); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(uncomplen, uncomplen2) << "Uncompression mismatch!"; + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen)) + << "Uncompression mismatch!"; + if (tmpbuf) free(tmpbuf); +} + +void TestChunkedGzip(ZLib* zlib, const std::string& uncompbuf_str, + int num_chunks) { + const char* uncompbuf = uncompbuf_str.data(); + const uLongf uncomplen = uncompbuf_str.size(); + std::string compbuf(MAX_BUF_SIZE, '\0'); + std::string uncompbuf2(MAX_BUF_FLEX, '\0'); + CHECK_GT(num_chunks, 2); + + // uncompbuf2 is larger than uncompbuf to test for decoding too much and for + // historical reasons. + // + // Note that it is possible to receive num_chunks+1 total + // chunks, due to rounding error. + const int chunklen = uncomplen / num_chunks; + int chunknum, i, err; + int cum_len[num_chunks + 10]; // cumulative compressed length + cum_len[0] = 0; + for (chunknum = 0, i = 0; i < uncomplen; i += chunklen, chunknum++) { + uLongf complen = compbuf.size() - cum_len[chunknum]; + // Make sure the last chunk gets the correct chunksize. + int chunksize = (uncomplen - i) < chunklen ? (uncomplen - i) : chunklen; + err = zlib->CompressChunk((Bytef*)compbuf.data() + cum_len[chunknum], + &complen, (Bytef*)uncompbuf + i, chunksize); + ASSERT_EQ(Z_OK, err) << " " << uncomplen << " bytes down to " << complen + << " bytes."; + cum_len[chunknum + 1] = cum_len[chunknum] + complen; + } + uLongf complen = compbuf.size() - cum_len[chunknum]; + err = zlib->CompressChunkDone((Bytef*)compbuf.data() + cum_len[chunknum], + &complen); + EXPECT_EQ(Z_OK, err); + cum_len[chunknum + 1] = cum_len[chunknum] + complen; + + for (chunknum = 0, i = 0; i < uncomplen; i += chunklen, chunknum++) { + uLongf uncomplen2 = uncomplen - i; + // Make sure the last chunk gets the correct chunksize. + int expected = uncomplen2 < chunklen ? uncomplen2 : chunklen; + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0] + i, &uncomplen2, + (Bytef*)compbuf.data() + cum_len[chunknum], + cum_len[chunknum + 1] - cum_len[chunknum]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(expected, uncomplen2) + << "Uncompress size is " << uncomplen2 << ", not " << expected; + } + // There should be no further uncompressed bytes, after uncomplen bytes. + uLongf uncomplen2 = uncompbuf2.size() - uncomplen; + EXPECT_NE(0, uncomplen2); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0] + uncomplen, &uncomplen2, + (Bytef*)compbuf.data() + cum_len[chunknum], + cum_len[chunknum + 1] - cum_len[chunknum]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(0, uncomplen2); + EXPECT_TRUE(zlib->UncompressChunkDone()); + + // Those uncomplen bytes should match. + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen)) + << "Uncompression mismatch!"; + + // Now test to make sure resetting works properly + // (1) First, uncompress the first chunk and make sure it's ok + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), cum_len[1]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(chunklen, uncomplen2) << "Uncompression mismatch!"; + // The first uncomplen2 bytes should match, where uncomplen2 is the number of + // successfully uncompressed bytes by the most recent UncompressChunk call. + // The remaining (uncomplen - uncomplen2) bytes would still match if the + // uncompression guaranteed not to modify the buffer other than those first + // uncomplen2 bytes, but there is no such guarantee. + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // (2) Now, try the first chunk again and see that there's an error + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), cum_len[1]); + EXPECT_EQ(Z_DATA_ERROR, err); + + // (3) Now reset it and try again, and see that it's ok + zlib->Reset(); + uncomplen2 = uncompbuf2.size(); + err = zlib->UncompressChunk((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)compbuf.data(), cum_len[1]); + EXPECT_EQ(Z_OK, err); + EXPECT_EQ(chunklen, uncomplen2) << "Uncompression mismatch!"; + EXPECT_EQ(0, memcmp(uncompbuf, uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + // (4) Make sure we can tackle output buffers that are too small + // with the *AtMost() interfaces. + uLong source_len = cum_len[2] - cum_len[1]; + CHECK_GT(source_len, 1); + uncomplen2 = source_len / 2; + err = zlib->UncompressAtMost((Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)(compbuf.data() + cum_len[1]), + &source_len); + EXPECT_EQ(Z_BUF_ERROR, err); + + EXPECT_EQ(0, memcmp(uncompbuf + chunklen, uncompbuf2.data(), uncomplen2)) + << "Uncompression mismatch!"; + + const int saveuncomplen2 = uncomplen2; + uncomplen2 = uncompbuf2.size() - uncomplen2; + // Uncompress the rest of the chunk. + err = zlib->UncompressAtMost( + (Bytef*)&uncompbuf2[0], &uncomplen2, + (Bytef*)(compbuf.data() + cum_len[2] - source_len), &source_len); + + EXPECT_EQ(Z_OK, err); + + EXPECT_EQ(0, memcmp(uncompbuf + chunklen + saveuncomplen2, uncompbuf2.data(), + uncomplen2)) + << "Uncompression mismatch!"; + + // (5) Finally, reset again so the rest of the tests can succeed. :-) + zlib->Reset(); +} + +void TestFooterBufferTooSmall(ZLib* zlib) { + uLongf footer_len = zlib->MinFooterSize() - 1; + ASSERT_EQ(9, footer_len); + Bytef footer_buffer[footer_len]; + int err = zlib->CompressChunkDone(footer_buffer, &footer_len); + ASSERT_EQ(Z_BUF_ERROR, err); + ASSERT_EQ(0, footer_len); +} + +// Helper routine for running a program and capturing its output +std::string RunCommand(const std::string& cmd) { + LOG(INFO) << "Running [" << cmd << "]"; + FILE* f = popen(cmd.c_str(), "r"); + CHECK(f != nullptr) << ": " << cmd << " failed"; + std::string result; + while (!feof(f) && !ferror(f)) { + char buf[1000]; + int n = fread(buf, 1, sizeof(buf), f); + CHECK(n >= 0); + result.append(buf, n); + } + CHECK(!ferror(f)); + pclose(f); + return result; +} + +// Helper routine to get uncompressed format of a string +std::string UncompressString(const std::string& input) { + Bytef* dest; + uLongf dest_len = FLAGS_zlib_max_size_uncompressed_data; + ZLib z; + z.SetGzipHeaderMode(); + int err = z.UncompressGzipAndAllocate(&dest, &dest_len, (Bytef*)input.data(), + input.size()); + CHECK_EQ(err, Z_OK); + std::string result((char*)dest, dest_len); + free(dest); + return result; +} + +class ZLibWrapperTest : public ::testing::TestWithParam { + protected: + // Returns the dictionary to use in our tests. If --dict is specified, the + // file pointed to by that flag is read in and used as the dictionary. + // Otherwise, a short default dictionary is used. + std::string GetDict() { + std::string dict; + const long kMaxDictLen = 32768; + + // Read in dictionary if specified, else use a default one. + if (!FLAGS_dict.empty()) { + CHECK(ReadFileToString(FLAGS_dict, &dict, kMaxDictLen)); + LOG(INFO) << "Read dictionary from " << FLAGS_dict << " (size " + << dict.size() << ")."; + } else { + dict = "this is a sample dictionary of the and or but not We URL"; + LOG(INFO) << "Using built-in dictionary (size " << dict.size() << ")."; + } + return dict; + } + + std::string ReadFileToTest(const std::string& filename) { + std::string uncompbuf; + LOG(INFO) << "Testing file: " << filename; + CHECK(ReadFileToString(filename, &uncompbuf, MAX_BUF_SIZE)); + return uncompbuf; + } +}; + +TEST(ZLibWrapperTest, HugeCompression) { + SlowTestLimiter limiter; + if (limiter.IsSlowBuild()) { + LOG(WARNING) << "Skipping test. Reason: " << limiter.reason(); + return; + } + + int lvl = FLAGS_min_comp_lvl; + + // Just big enough to trigger 32 bit overflow in MinCompressbufSize() + // calculation. + const uLong HUGE_DATA_SIZE = 0x81000000; + + // Construct an easily compressible huge buffer. + std::string uncompbuf(HUGE_DATA_SIZE, 'A'); + + LOG(INFO) << "Huge compression at level " << lvl; + ZLib zlib; + zlib.SetCompressionLevel(lvl); + TestCompression(&zlib, uncompbuf, nullptr); +} + +TEST_P(ZLibWrapperTest, Compression) { + const std::string dict = GetDict(); + const std::string uncompbuf = ReadFileToTest(GetParam()); + + for (int lvl = FLAGS_min_comp_lvl; lvl <= FLAGS_max_comp_lvl; lvl++) { + for (int no_header_mode = 0; no_header_mode <= 1; no_header_mode++) { + ZLib zlib; + zlib.SetCompressionLevel(lvl); + zlib.SetNoHeaderMode(no_header_mode); + + // TODO(gromer): Restructure the following code to minimize use of helper + // functions and LOG(INFO). + LOG(INFO) << "Level " << lvl << ", no_header_mode " << no_header_mode + << " (No dict)"; + TestCompression(&zlib, uncompbuf, " No dict"); + LOG(INFO) << "Level " << lvl << ", no_header_mode " << no_header_mode; + TestCompression(&zlib, uncompbuf, nullptr); + + // Try with a dictionary. For reasons I don't entirely understand, + // no_header_mode does not coexist with preloaded dictionaries. + if (!no_header_mode) { // try it with a dictionary + char dict_msg[64]; + snprintf(dict_msg, sizeof(dict_msg), " Dict %u", + static_cast(dict.size())); + zlib.SetDictionary(dict.data(), dict.size()); + LOG(INFO) << "Level " << lvl << " dict: " << dict_msg; + TestCompression(&zlib, uncompbuf, dict_msg); + LOG(INFO) << "Level " << lvl; + TestCompression(&zlib, uncompbuf, nullptr); + } + } + } +} + +// Make sure we deal correctly with a bug in old versions of zlibwrapper +TEST_P(ZLibWrapperTest, BuggyCompression) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + LOG(INFO) << "workaround for old zlibwrapper bug"; + TestBuggyCompression(&zlib, uncompbuf); + + // Try compressing again using the same ZLib + LOG(INFO) << "workaround for old zlibwrapper bug: same ZLib"; + TestBuggyCompression(&zlib, uncompbuf); +} + +// Test other problems +TEST_P(ZLibWrapperTest, OtherErrors) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + zlib.SetNoHeaderMode(false); + LOG(INFO) + << "Testing robustness against various errors: no_header_mode = false"; + TestErrors(&zlib, uncompbuf); + + zlib.SetNoHeaderMode(true); + LOG(INFO) + << "Testing robustness against various errors: no_header_mode = true"; + TestErrors(&zlib, uncompbuf); + + zlib.SetGzipHeaderMode(); + LOG(INFO) << "Testing robustness against various errors: gzip_header_mode"; + TestErrors(&zlib, uncompbuf); + + LOG(INFO) + << "Testing robustness against various errors: bogus gunzip request"; + TestBogusGunzipRequest(&zlib); +} + +// Make sure that (Un-)compress returns a correct error when asked to +// (un-)compress into a buffer bigger than 2^32 bytes. +// Running this with blaze --config=msan exposed the bug underlying +// http://b/25308089. +TEST_P(ZLibWrapperTest, TestBuffersTooBigFails) { + uLongf valid_len = 100; + uLongf invalid_len = 5000000000; // Bigger than 32 bit supported by zlib. + const Bytef* data = reinterpret_cast("test"); + uLongf data_len = 5; + // This test is not reusing the Zlib object so msan can determine + // when it's used uninitialized. + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Compress(nullptr, &invalid_len, data, data_len)); + } + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Compress(nullptr, &valid_len, nullptr, invalid_len)); + } + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Uncompress(nullptr, &invalid_len, data, data_len)); + } + { + ZLib zlib; + EXPECT_EQ(Z_BUF_ERROR, + zlib.Uncompress(nullptr, &valid_len, nullptr, invalid_len)); + } +} + +// Make sure we deal correctly with compressed headers chunked weirdly +TEST_P(ZLibWrapperTest, UncompressChunked) { + { + ZLib zlib; + zlib.SetGzipHeaderMode(); + LOG(INFO) << "Uncompressing gzip headers"; + TestGzipHeaderUncompress(&zlib); + } + { + ZLib zlib; + zlib.SetGzipHeaderMode(); + LOG(INFO) << "Uncompressing randomly-fragmented gzip headers"; + TestRandomGzipHeaderUncompress(&zlib); + } +} + +// Now test gzip compression. +TEST_P(ZLibWrapperTest, GzipCompression) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + zlib.SetGzipHeaderMode(); + LOG(INFO) << "gzip compression"; + TestGzip(&zlib, uncompbuf); + + // Try compressing again using the same ZLib + LOG(INFO) << "gzip compression: same ZLib"; + TestGzip(&zlib, uncompbuf); +} + +// Now test chunked compression. +TEST_P(ZLibWrapperTest, ChunkedCompression) { + const std::string uncompbuf = ReadFileToTest(GetParam()); + ZLib zlib; + + zlib.SetGzipHeaderMode(); + LOG(INFO) << "chunked gzip compression"; + // At this point is the minimum between MAX_BUF_SIZE (1048500) + // and the size of the last input file processed. With a larger file, + // uncompen is a multiple of both 10, 20 and 100. + // Using 21 chunks cause the last chunk to be smaller than the others. + TestChunkedGzip(&zlib, uncompbuf, 21); + + // Try compressing again using the same ZLib + LOG(INFO) << "chunked gzip compression: same ZLib"; + TestChunkedGzip(&zlib, uncompbuf, 20); + + // In theory we can mix and match the type of compression we do + LOG(INFO) << "chunked gzip compression: different compression type"; + TestGzip(&zlib, uncompbuf); + LOG(INFO) << "chunked gzip compression: original compression type"; + TestChunkedGzip(&zlib, uncompbuf, 100); + + // Test writing final chunk and footer into buffer that's too small. + LOG(INFO) << "chunked gzip compression: buffer too small"; + TestFooterBufferTooSmall(&zlib); + + LOG(INFO) << "chunked gzip compression: not chunked"; + TestGzip(&zlib, uncompbuf); +} + +// Simple helper to force specialization of strings::Split. +std::vector GetFilesToProcess() { + std::string files_to_process = + FLAGS_files_to_process.empty() + ? absl::StrCat(FLAGS_test_srcdir, "/google3/util/gtl/testdata/words") + : FLAGS_files_to_process; + return absl::StrSplit(files_to_process, ",", absl::SkipWhitespace()); +} + +INSTANTIATE_TEST_SUITE_P(AllTests, ZLibWrapperTest, + ::testing::ValuesIn(GetFilesToProcess())); + +TEST(ZLibWrapperStandaloneTest, GzipCompatibility) { + LOG(INFO) << "Testing compatibility with gzip output"; + const std::string input = "hello world"; + std::string gzip_output = + RunCommand(absl::StrCat("echo ", input, " | gzip -c")); + ASSERT_EQ(absl::StrCat(input, "\n"), UncompressString(gzip_output)); +} + +/* + The Gzip footer contains the lower four bytes of the uncompressed length. + Previously IsGzipFooterValid() compared the value from the footer with the + entire length, not the lower four bytes of the length. + + To test this, compress a 4 GB file a chunk at a time. + */ +TEST(ZLibWrapperStandaloneTest, DecompressHugeFileWithFooter) { + SlowTestLimiter limiter; + + ZLib compressor; + // We specifically want to test that we validate the footer correctly. + compressor.SetGzipHeaderMode(); + + ZLib decompressor; + decompressor.SetGzipHeaderMode(); + + const int64 uncompressed_size = 1LL << 32; // too big for a 4-byte int + int64 uncompressed_bytes_sent = 0; + + const int64 chunk_size = 10 * 1024 * 1024; + std::string inbuf(chunk_size, '\0'); // The input data + std::string compbuf(chunk_size, '\0'); // The compressed data + std::string outbuf(chunk_size, '\0'); // The output data + while (uncompressed_bytes_sent < uncompressed_size) { + if (limiter.DeadlineExceeded()) { + LOG(WARNING) << "Ending test early, after " << uncompressed_bytes_sent + << " of " << uncompressed_size + << " bytes. Reason: " << limiter.reason(); + return; + } + + // Compress a chunk. + uLongf complen = chunk_size; + ASSERT_EQ(Z_OK, + compressor.CompressChunk((Bytef*)compbuf.data(), &complen, + (Bytef*)inbuf.data(), inbuf.size())); + + // Uncompress a chunk. + uLongf outlen = chunk_size; + ASSERT_EQ(Z_OK, + decompressor.UncompressChunk((Bytef*)outbuf.data(), &outlen, + (Bytef*)compbuf.data(), complen)); + + ASSERT_EQ(outlen, inbuf.size()); + uncompressed_bytes_sent += inbuf.size(); + } + + // Write the footer chunk. + uLongf complen = chunk_size; + ASSERT_EQ(Z_OK, + compressor.CompressChunkDone((Bytef*)compbuf.data(), &complen)); + + // Read the footer chunk. + uLongf outlen = chunk_size; + ASSERT_EQ(Z_OK, + decompressor.UncompressChunk((Bytef*)outbuf.data(), &outlen, + (Bytef*)compbuf.data(), complen)); + + // This will fail if we validate the footer incorrectly. + ASSERT_TRUE(decompressor.UncompressChunkDone()); +} + +/* + Try to reproduce a bug in deflate, by repeatedly allocating compressors + and compressing a particular block of data (a Google News homepage that + I extracted from the core file of a crashed NFE). + + To see the bug you'll need to run an optimized build of the unittest (so + that malloc doesn't add headers) and remove the workaround from deflate.c + (see the comment in deflateInit2_ for details). + + The full story: + + The inner loop of deflate is in the function longest_match, comparing two + byte strings to find the length of the common prefix up to a maximum length + of 258. The string being examined is in a 64K byte buffer that zlib + allocates internally (called "window"); the data to be compressed is streamed + into it. The code that calls longest_match (deflate_slow) ensures that there + are always at least MIN_LOOKAHEAD (=262) bytes in the window beyond the start + of the string being examined. + + For performance longest_match has been rewritten in assembler (match.S), and + the inner loop compares 8 bytes at a time. The loop is written to always + examine 264 bytes, starting within a few bytes of the start of the string + (depending on alignment). If you start with a string of length 263 right at + the end of the buffer, you end up looking at a byte or two beyond the end of + the buffer. It doesn't matter whether those bytes match or not, since the + match length will get maxed against 258 anyway, but if you're unlucky and + the page after the buffer isn't mapped, you'll die. + + This never happened to GWS because it doesn't generate pages that are 64K + long. It doesn't happen to anyone outside google because everyone else's + malloc, when asked to alloc a 64K block, will actually allocate an extra + page to allow for the headers. But the NFE, a google front-end that + routinely generates 120K result pages, hit this bug about 20 times a day. +*/ +TEST(ZLibWrapperStandalone, ReadPastEndOfWindow) { + SlowTestLimiter limiter; + + std::string fname = FLAGS_read_past_window_data_file; + if (fname.empty()) { + fname = FLAGS_test_srcdir + + "/google3/third_party/zlib/testdata/read_past_window.data"; + } + std::string uncompbuf; + ASSERT_TRUE(ReadFileToString(fname, &uncompbuf, MAX_BUF_SIZE)); + const uLongf uncomplen = uncompbuf.size(); + ASSERT_TRUE(uncomplen >= 0x10000) << "not enough test data in " << fname; + + std::vector> used_zlibs; + unsigned long comprlen = ZLib::MinCompressbufSize(uncomplen); + std::string compr(comprlen, '\0'); + + for (int i = 0; i < FLAGS_read_past_window_iterations; ++i) { + if (limiter.DeadlineExceeded()) { + LOG(WARNING) << "Ending test after only " << i + << " of --read_past_window_iteratons=" + << FLAGS_read_past_window_iterations + << " iterations. Reason: " << limiter.reason(); + break; + } + + ZLib* zlib = new ZLib; + zlib->SetGzipHeaderMode(); + int rc = zlib->Compress((Bytef*)&compr[0], &comprlen, + (Bytef*)uncompbuf.data(), uncomplen); + ASSERT_EQ(rc, Z_OK); + used_zlibs.emplace_back(zlib); + } + + // if we haven't segfaulted by now, we pass + LOG(INFO) << "passed read-past-end-of-window test"; +} + +} // namespace +} // namespace firebase diff --git a/app/src/fake/FIRApp.h b/app/src/fake/FIRApp.h new file mode 100644 index 0000000000..70d81b1219 --- /dev/null +++ b/app/src/fake/FIRApp.h @@ -0,0 +1,58 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import + +extern "C" { + +// Test method to create an application using the specified name and default +// options. +void FIRAppCreateUsingDefaultOptions(const char* name); +// Test method to clear all app instances. +void FIRAppResetApps(); + +} + +@class FIROptions; + +typedef void (^FIRAppVoidBoolCallback)(BOOL success); + +/** + * A fake Firebase App class for unit-testing. + */ +@interface FIRApp : NSObject + +// Test method to clear all FIRApp instances. ++ (void)resetApps; + ++ (void)configure; + ++ (void)configureWithOptions:(FIROptions *)options; + ++ (void)configureWithName:(NSString *)name options:(FIROptions *)options; + ++ (FIRApp *)defaultApp; + ++ (FIRApp *)appNamed:(NSString *)name; + +- (void)deleteApp:(FIRAppVoidBoolCallback)completion; + +@property(nonatomic, copy, readonly) NSString *name; + +@property(nonatomic, copy, readonly) FIROptions *options; + +@property(nonatomic, readwrite, getter=isDataCollectionDefaultEnabled) + BOOL dataCollectionDefaultEnabled; + +@end diff --git a/app/src/fake/FIRApp.mm b/app/src/fake/FIRApp.mm new file mode 100644 index 0000000000..8657edf9d0 --- /dev/null +++ b/app/src/fake/FIRApp.mm @@ -0,0 +1,106 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "app/src/fake/FIRApp.h" + +#import "app/src/fake/FIROptions.h" + +static NSString *kFIRDefaultAppName = @"__FIRAPP_DEFAULT"; + +@implementation FIRApp + +@synthesize options = _options; +BOOL _dataCollectionEnabled; + +static NSMutableDictionary *sAllApps; + +- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options { + self = [super init]; + if (self) { + _name = [name copy]; + _options = [options copy]; + _dataCollectionEnabled = YES; + } + return self; +} + ++ (void)resetApps { + if (sAllApps) [sAllApps removeAllObjects]; +} + ++ (void)configure { + return [FIRApp configureWithOptions:[FIROptions defaultOptions]]; +} + ++ (void)configureWithOptions:(FIROptions *)options { + return [FIRApp configureWithName:kFIRDefaultAppName options:options]; +} + ++ (void)configureWithName:(NSString *)name options:(FIROptions *)options { + FIRApp *app = [[FIRApp alloc] initInstanceWithName:name options:options]; + if (!sAllApps) sAllApps = [[NSMutableDictionary alloc] init]; + sAllApps[app.name] = app; +} + ++ (FIRApp *)defaultApp { + return sAllApps ? sAllApps[kFIRDefaultAppName] : nil; +} + ++ (FIRApp *)appNamed:(NSString *)name { + return sAllApps ? sAllApps[name] : nil; +} + +- (void)deleteApp:(FIRAppVoidBoolCallback)completion { + if (sAllApps) { + [sAllApps removeObjectForKey:self.name]; + } + completion(TRUE); +} + +- (void)setDataCollectionDefaultEnabled:(BOOL)dataCollectionDefaultEnabled { + _dataCollectionEnabled = dataCollectionDefaultEnabled; +} + +- (BOOL)isDataCollectionDefaultEnabled { + return _dataCollectionEnabled; +} + +static NSMutableDictionary* sRegisteredLibraries = [[NSMutableDictionary alloc] init]; + ++ (void)registerLibrary:(nonnull NSString *)library withVersion:(nonnull NSString *)version { + if (sRegisteredLibraries.count == 0) sRegisteredLibraries[@"fire-ios"] = @"1.2.3"; + sRegisteredLibraries[library] = version; +} + ++ (NSString *)firebaseUserAgent { + NSMutableArray *libraries = + [[NSMutableArray alloc] initWithCapacity:sRegisteredLibraries.count]; + for (NSString *libraryName in sRegisteredLibraries) { + [libraries + addObject:[NSString stringWithFormat:@"%@/%@", libraryName, + sRegisteredLibraries[libraryName]]]; + } + [libraries sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + return [libraries componentsJoinedByString:@" "]; +} + +@end + +void FIRAppCreateUsingDefaultOptions(const char* name) { + [FIRApp configureWithName:@(name) options:[FIROptions defaultOptions]]; +} + +void FIRAppResetApps() { + [FIRApp resetApps]; +} diff --git a/app/src/fake/FIRConfiguration.h b/app/src/fake/FIRConfiguration.h new file mode 100644 index 0000000000..2b8e678ceb --- /dev/null +++ b/app/src/fake/FIRConfiguration.h @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRLoggerLevel.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This interface provides global level properties that the developer can tweak. + */ +NS_SWIFT_NAME(FirebaseConfiguration) +@interface FIRConfiguration : NSObject + +/** Returns the shared configuration object. */ +@property(class, nonatomic, readonly) FIRConfiguration *sharedInstance NS_SWIFT_NAME(shared); + +/** + * Sets the logging level for internal Firebase logging. Firebase will only log messages + * that are logged at or below loggerLevel. The messages are logged both to the Xcode + * console and to the device's log. Note that if an app is running from AppStore, it will + * never log above FIRLoggerLevelNotice even if loggerLevel is set to a higher (more verbose) + * setting. + * + * @param loggerLevel The maximum logging level. The default level is set to FIRLoggerLevelNotice. + */ +- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/app/src/fake/FIRConfiguration.m b/app/src/fake/FIRConfiguration.m new file mode 100644 index 0000000000..49cc8a17ed --- /dev/null +++ b/app/src/fake/FIRConfiguration.m @@ -0,0 +1,34 @@ +// Copyright 2017 Google +// +// 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. + +#import "FIRConfiguration.h" + +@implementation FIRConfiguration + ++ (instancetype)sharedInstance { + static FIRConfiguration *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FIRConfiguration alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + return [super init]; +} + +- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel {} + +@end diff --git a/app/src/fake/FIRLogger.h b/app/src/fake/FIRLogger.h new file mode 100644 index 0000000000..de50235616 --- /dev/null +++ b/app/src/fake/FIRLogger.h @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#import + +#import "FIRLoggerLevel.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Changes the default logging level of FIRLoggerLevelNotice to a user-specified level. + * The default level cannot be set above FIRLoggerLevelNotice if the app is running from App Store. + * (required) log level (one of the FIRLoggerLevel enum values). + */ +void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel); + +#ifdef __cplusplus +} +#endif // __cplusplus diff --git a/app/src/fake/FIRLoggerLevel.h b/app/src/fake/FIRLoggerLevel.h new file mode 100644 index 0000000000..dca3aa0b01 --- /dev/null +++ b/app/src/fake/FIRLoggerLevel.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +// Note that importing GULLoggerLevel.h will lead to a non-modular header +// import error. + +/** + * The log levels used by internal logging. + */ +typedef NS_ENUM(NSInteger, FIRLoggerLevel) { + /** Error level, matches ASL_LEVEL_ERR. */ + FIRLoggerLevelError = 3, + /** Warning level, matches ASL_LEVEL_WARNING. */ + FIRLoggerLevelWarning = 4, + /** Notice level, matches ASL_LEVEL_NOTICE. */ + FIRLoggerLevelNotice = 5, + /** Info level, matches ASL_LEVEL_INFO. */ + FIRLoggerLevelInfo = 6, + /** Debug level, matches ASL_LEVEL_DEBUG. */ + FIRLoggerLevelDebug = 7, + /** Minimum log level. */ + FIRLoggerLevelMin = FIRLoggerLevelError, + /** Maximum log level. */ + FIRLoggerLevelMax = FIRLoggerLevelDebug +} NS_SWIFT_NAME(FirebaseLoggerLevel); diff --git a/app/src/fake/FIROptions.h b/app/src/fake/FIROptions.h new file mode 100644 index 0000000000..b9a07cb214 --- /dev/null +++ b/app/src/fake/FIROptions.h @@ -0,0 +1,51 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import + +/** + * A fake Firebase Option class for unit-testing. + */ +@interface FIROptions : NSObject + +@property(nonatomic, copy) NSString *APIKey; + +@property(nonatomic, copy) NSString *bundleID; + +@property(nonatomic, copy) NSString *clientID; + +@property(nonatomic, copy) NSString *trackingID; + +@property(nonatomic, copy) NSString *GCMSenderID; + +@property(nonatomic, copy) NSString *projectID; + +@property(nonatomic, copy) NSString *androidClientID; + +@property(nonatomic, copy) NSString *googleAppID; + +@property(nonatomic, copy) NSString *databaseURL; + +@property(nonatomic, copy) NSString *deepLinkURLScheme; + +@property(nonatomic, copy) NSString *storageBucket; + +@property(nonatomic, copy) NSString *appGroupID; + ++ (FIROptions *)defaultOptions; + +- (instancetype)initWithGoogleAppID:(NSString *)googleAppID + GCMSenderID:(NSString *)GCMSenderID; + +@end diff --git a/app/src/fake/FIROptions.mm b/app/src/fake/FIROptions.mm new file mode 100644 index 0000000000..c3042d2829 --- /dev/null +++ b/app/src/fake/FIROptions.mm @@ -0,0 +1,66 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "app/src/fake/FIROptions.h" + +@implementation FIROptions + ++ (FIROptions *)defaultOptions { + FIROptions* options = + [[FIROptions alloc] initWithGoogleAppID:@"fake google app id from resource" + GCMSenderID:@"fake messaging sender id from resource"]; + options.APIKey = @"fake api key from resource"; + options.bundleID = @"fake bundle ID from resource"; + options.clientID = @"fake client id from resource"; + options.trackingID = @"fake ga tracking id from resource"; + options.projectID = @"fake project id from resource"; + options.androidClientID = @"fake android client id from resource"; + options.googleAppID = @"fake app id from resource"; + options.databaseURL = @"fake database url from resource"; + options.deepLinkURLScheme = @"fake deep link url scheme from resource"; + options.storageBucket = @"fake storage bucket from resource"; + return options; +} + +- (instancetype)initWithGoogleAppID:(NSString *)googleAppID + GCMSenderID:(NSString *)GCMSenderID { + self = [super init]; + if (self) { + _googleAppID = googleAppID; + _GCMSenderID = GCMSenderID; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + FIROptions *newOptions = [[[self class] allocWithZone:zone] init]; + if (newOptions) { + newOptions.googleAppID = self.googleAppID; + newOptions.GCMSenderID = self.GCMSenderID; + newOptions.APIKey = self.APIKey; + newOptions.bundleID = self.bundleID; + newOptions.clientID = self.clientID; + newOptions.trackingID = self.trackingID; + newOptions.projectID = self.projectID; + newOptions.androidClientID = self.androidClientID; + newOptions.googleAppID = self.googleAppID; + newOptions.databaseURL = self.databaseURL; + newOptions.deepLinkURLScheme = self.deepLinkURLScheme; + newOptions.storageBucket = self.storageBucket; + newOptions.appGroupID = self.appGroupID; + } + return newOptions; +} + +@end diff --git a/app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java b/app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java new file mode 100644 index 0000000000..9bdfa3ab52 --- /dev/null +++ b/app/src_java/fake/com/google/android/gms/common/GoogleApiAvailability.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.android.gms.common; + +import android.content.Context; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; + +/** Fake gms/common/GoogleApiAvailability.java for unit testing. */ +public final class GoogleApiAvailability { + + private static final GoogleApiAvailability INSTANCE = new GoogleApiAvailability(); + + public static GoogleApiAvailability getInstance() { + ConfigRow row = ConfigAndroid.get("GoogleApiAvailability.getInstance"); + if (row != null) { + // Right now we let it returns null and ignore whatever set. + return null; + } + + // Default behavior + return INSTANCE; + } + + public int isGooglePlayServicesAvailable(Context context) { + ConfigRow row = ConfigAndroid.get("GoogleApiAvailability.isGooglePlayServicesAvailable"); + if (row != null) { + return row.futureint().value(); + } + + // Default behavior + return 0; + } +} diff --git a/app/src_java/fake/com/google/android/gms/tasks/Task.java b/app/src_java/fake/com/google/android/gms/tasks/Task.java new file mode 100644 index 0000000000..04a3b4cbe9 --- /dev/null +++ b/app/src_java/fake/com/google/android/gms/tasks/Task.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.android.gms.tasks; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeListener; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import com.google.firebase.testing.cppsdk.TickerObserver; +import java.util.Vector; + +/** + * Fake Task class that accepts instruction from {@link setEta}, {@link setResult} and {@link + * setException} and acts accordingly. + */ +public class Task implements TickerObserver { + private final Vector> mListenerQueue = new Vector<>(); + private long eta; + private TResult mResult; + private Exception mException; + + public TResult getResult() throws Exception { + if (mException != null) { + throw mException; + } + return mResult; + } + + @Override + public void elapse() { + if (isComplete() && !mListenerQueue.isEmpty()) { + for (FakeListener listener : mListenerQueue) { + if (mException == null) { + listener.onSuccess(mResult); + } else { + listener.onFailure(mException); + } + } + mListenerQueue.clear(); + } + } + + public boolean isComplete() { + return eta <= TickerAndroid.now(); + } + + public boolean isSuccessful() { + return isComplete() && mException == null; + } + + public void setEta(long eta) { + this.eta = eta; + } + + /** Set what result the task should return unless you also call {@link setException}. */ + public void setResult(TResult result) { + mResult = result; + } + + /** Set an exception the task should throw. */ + public void setException(Exception e) { + mException = e; + } + + /** + * To make writing fake less cumbersome, we use a single type of {@link FakeListener} to mimic all + * types of listeners. + */ + public Task addListener(FakeListener listener) { + mListenerQueue.add(listener); + elapse(); + return this; + } + + /** A helper function to get a task that returns immediately the specified result. */ + public static Task forResult(TResult result) { + Task task = new Task<>(); + task.setResult(result); + task.setEta(0L); + return task; + } + + /** A helper function to get a task from a {@link ConfigRow}. */ + public static Task forResult(String configKey, TResult result) { + ConfigRow row = ConfigAndroid.get(configKey); + if (row == null) { + // Default behavior when no config is set. + return forResult(result); + } + + Task task = new Task<>(); + if (row.futuregeneric().throwexception()) { + task.setException(new Exception(row.futuregeneric().exceptionmsg())); + } else { + task.setResult(result); + } + task.setEta(row.futuregeneric().ticker()); + return task; + } +} diff --git a/app/src_java/fake/com/google/firebase/FirebaseApp.java b/app/src_java/fake/com/google/firebase/FirebaseApp.java new file mode 100644 index 0000000000..da148ca1d6 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/FirebaseApp.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +import android.content.Context; +import java.util.HashMap; + +/** Fake //j/c/g/a/gmscore/integ/client/firebase_common/src/com/google/firebase/FirebaseApp.java */ +public final class FirebaseApp { + static final String DEFAULT_NAME = "[DEFAULT]"; + static final HashMap instances = new HashMap<>(); + + String name; + FirebaseOptions options; + + // Exposed to clear all FirebaseApp instances. This should be called between each test case. + public static void reset() { + instances.clear(); + } + + public static FirebaseApp initializeApp(Context context, FirebaseOptions options) { + return initializeApp(context, options, DEFAULT_NAME); + } + + public static FirebaseApp initializeApp(Context context, FirebaseOptions options, String name) { + if (!instances.containsKey(name)) { + instances.put(name, new FirebaseApp(name, options)); + } + return getInstance(name); + } + + public static FirebaseApp getInstance() { + return getInstance(DEFAULT_NAME); + } + + public static FirebaseApp getInstance(String name) { + FirebaseApp app = instances.get(name); + if (app == null) { + throw new IllegalStateException(String.format("FirebaseApp %s does not exist", name)); + } + return app; + } + + private FirebaseApp(String name, FirebaseOptions options) { + this.name = name; + this.options = options; + } + + public void delete() { + instances.remove(name); + } + + public FirebaseOptions getOptions() { + return options; + } + + public boolean isDataCollectionDefaultEnabled() { + return true; + } + + public void setDataCollectionDefaultEnabled(boolean enabled) {} +} diff --git a/app/src_java/fake/com/google/firebase/FirebaseException.java b/app/src_java/fake/com/google/firebase/FirebaseException.java new file mode 100644 index 0000000000..8d430cd072 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/FirebaseException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseException */ +public class FirebaseException extends Exception { + + public FirebaseException(String message) { + super(message); + } +} diff --git a/app/src_java/fake/com/google/firebase/FirebaseOptions.java b/app/src_java/fake/com/google/firebase/FirebaseOptions.java new file mode 100644 index 0000000000..7559eee97f --- /dev/null +++ b/app/src_java/fake/com/google/firebase/FirebaseOptions.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +import android.content.Context; +import android.util.Log; + +/** + * Fake //j/c/g/a/gmscore/integ/client/firebase_common/src/com/google/firebase/FirebaseOptions.java + */ +public final class FirebaseOptions { + private static final String LOG_TAG = "FakeFirebaseOptions"; + + /** Fake Builder. */ + public static final class Builder { + private String apiKey; + private String applicationId; + private String databaseUrl; + private String gcmSenderId; + private String storageBucket; + private String projectId; + + public Builder setApiKey(String apiKey) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set api key " + apiKey); + } + this.apiKey = apiKey; + return this; + } + + public Builder setDatabaseUrl(String databaseUrl) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set database url " + databaseUrl); + } + this.databaseUrl = databaseUrl; + return this; + } + + public Builder setApplicationId(String applicationId) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set application id " + applicationId); + } + this.applicationId = applicationId; + return this; + } + + public Builder setGcmSenderId(String gcmSenderId) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set gcm sender id " + gcmSenderId); + } + this.gcmSenderId = gcmSenderId; + return this; + } + + public Builder setStorageBucket(String storageBucket) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set storage bucket " + storageBucket); + } + this.storageBucket = storageBucket; + return this; + } + + public Builder setProjectId(String projectId) { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "set project id " + projectId); + } + this.projectId = projectId; + return this; + } + + public FirebaseOptions build() { + if (Log.isLoggable(LOG_TAG, Log.INFO)) { + Log.i(LOG_TAG, "built"); + } + return new FirebaseOptions(this); + } + } + + public Builder builder; + + private FirebaseOptions(Builder builder) { + this.builder = builder; + } + + public static FirebaseOptions fromResource(Context context) { + return new FirebaseOptions( + new Builder() + .setApiKey("fake api key from resource") + .setDatabaseUrl("fake database url from resource") + .setApplicationId("fake app id from resource") + .setGcmSenderId("fake messaging sender id from resource") + .setStorageBucket("fake storage bucket from resource") + .setProjectId("fake project id from resource")); + } + + public String getApiKey() { + return builder.apiKey; + } + + public String getApplicationId() { + return builder.applicationId; + } + + public String getDatabaseUrl() { + return builder.databaseUrl; + } + + public String getGcmSenderId() { + return builder.gcmSenderId; + } + + public String getStorageBucket() { + return builder.storageBucket; + } + + public String getProjectId() { + return builder.projectId; + } +} diff --git a/app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java b/app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java new file mode 100644 index 0000000000..9837ef421a --- /dev/null +++ b/app/src_java/fake/com/google/firebase/app/internal/cpp/CppThreadDispatcher.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.app.internal.cpp; + +import android.app.Activity; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** Runs a native C++ function on an alternate thread. */ +public class CppThreadDispatcher { + private static final ExecutorService executor = Executors.newSingleThreadExecutor( + Executors.defaultThreadFactory()); + + /** Runs a C++ function on the main thread using the executor. */ + public static void runOnMainThread(Activity activity, final CppThreadDispatcherContext context) { + Object unused = executor.submit(new Runnable() { + @Override + public void run() { + context.execute(); + } + }); + } + + /** Runs a C++ function on a new Java background thread. */ + public static void runOnBackgroundThread(final CppThreadDispatcherContext context) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + context.execute(); + } + }); + t.start(); + } +} diff --git a/app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java b/app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java new file mode 100644 index 0000000000..ee34b2e859 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.app.internal.cpp; + +import android.app.Activity; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FutureBoolResult; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import com.google.firebase.testing.cppsdk.TickerObserver; + +/** + * Fake //f/a/c/cpp/src_java/com/google/firebase/app/internal/cpp/GoogleApiAvailabilityHelper.java + */ +public final class GoogleApiAvailabilityHelper { + private static final int SUCCESS = 0; + + public static boolean makeGooglePlayServicesAvailable(Activity activity) { + final ConfigRow row = + ConfigAndroid.get("GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable"); + if (row != null) { + TickerAndroid.register( + new TickerObserver() { + @Override + public void elapse() { + if (TickerAndroid.now() == row.futureint().ticker()) { + int resultCode = row.futureint().value(); + onCompleteNative(resultCode, "result code is " + resultCode); + } + } + }); + return row.futurebool().value() == FutureBoolResult.True; + } + + // Default behavior + onCompleteNative(SUCCESS, "Google Play services are already available (fake)"); + return true; + } + + public static void stopCallbacks() {} + + private static native void onCompleteNative(int resultCode, String resultMessage); +} diff --git a/app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java b/app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java new file mode 100644 index 0000000000..0be47b678f --- /dev/null +++ b/app/src_java/fake/com/google/firebase/app/internal/cpp/JniResultCallback.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.app.internal.cpp; + +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.FakeListener; + +/** + * Fake firebase/app/client/cpp/src_java/com/google/firebase/app/internal/cpp/JniResultCallback.java + */ +public class JniResultCallback { + private interface Callback { + public void register(); + + public void disconnect(); + }; + + private long callbackFn; + private long callbackData; + private Callback callbackHandler = null; + + public static final String LOG_TAG = "FakeFirebaseCb"; + + private class TaskCallback extends FakeListener implements Callback { + private Task task; + + public TaskCallback(Task task) { + this.task = task; + } + + @Override + public void onSuccess(T result) { + if (task != null) { + onCompletion(result, true, false, null); + } + disconnect(); + } + + @Override + public void onFailure(Exception exception) { + if (task != null) { + onCompletion(exception, false, false, exception.getMessage()); + } + disconnect(); + } + + @Override + public void register() { + task.addListener(this); + } + + @Override + public void disconnect() { + task = null; + } + } + + @SuppressWarnings("unchecked") + public JniResultCallback(Task task, long callbackFn, long callbackData) { + Log.i(LOG_TAG, String.format("JniResultCallback: Fn %x, Data %x", callbackFn, callbackData)); + this.callbackFn = callbackFn; + this.callbackData = callbackData; + callbackHandler = new TaskCallback<>(task); + callbackHandler.register(); + } + + public void cancel() { + Log.i(LOG_TAG, "canceled"); + onCompletion(null, false, true, "cancelled (fake)"); + } + + private void onCompletion( + Object result, boolean success, boolean cancelled, String statusMessage) { + if (callbackHandler != null) { + nativeOnResult( + result, success, cancelled, statusMessage, callbackFn, callbackData); + callbackHandler.disconnect(); + callbackHandler = null; + } + } + + private native void nativeOnResult( + Object result, + boolean success, + boolean cancelled, + String statusString, + long callbackFn, + long callbackData); +} diff --git a/app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java b/app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java new file mode 100644 index 0000000000..b5cf72cd61 --- /dev/null +++ b/app/src_java/fake/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +package com.google.firebase.platforminfo; + +import java.util.HashSet; +import java.util.Set; + +/** + * Fake + * //j/c/g/a/gmscore/integ/client/firebase_common/src/com/google/firebase/platforminfo/GlobalLibraryVersionRegistrar.java + */ +public final class GlobalLibraryVersionRegistrar { + public static GlobalLibraryVersionRegistrar getInstance() { + return new GlobalLibraryVersionRegistrar(); + } + + public void registerVersion(String library, String version) {} + + public Set getRegisteredVersions() { + return new HashSet<>(); + } +} diff --git a/app/tests/CMakeLists.txt b/app/tests/CMakeLists.txt new file mode 100644 index 0000000000..07bd6dea1b --- /dev/null +++ b/app/tests/CMakeLists.txt @@ -0,0 +1,410 @@ +# Copyright 2019 Google LLC +# +# 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. + +# Set up a library for create an app suitable for testing. Needs an empty +# source file, as not all compilers handle header only libraries with CMake. + +file(WRITE ${CMAKE_BINARY_DIR}/empty.cc) +add_library(firebase_app_for_testing + ${CMAKE_BINARY_DIR}/empty.cc + ${FIREBASE_SOURCE_DIR}/app/tests/include/firebase/app_for_testing.h +) + +target_include_directories(firebase_app_for_testing + PUBLIC + ${FIREBASE_SOURCE_DIR}/app/tests/include/firebase +) + +if (ANDROID) +elseif (IOS) + set(TEST_RUNNER_DIR "${FIREBASE_SOURCE_DIR}/app/src/tests/runner/ios") + add_executable(firebase_app_for_testing_ios MACOSX_BUNDLE + ${TEST_RUNNER_DIR}/FIRAppDelegate.m + ${TEST_RUNNER_DIR}/FIRAppDelegate.h + ${TEST_RUNNER_DIR}/FIRViewController.m + ${TEST_RUNNER_DIR}/FIRViewController.h + ${TEST_RUNNER_DIR}/main.m + ${FIREBASE_SOURCE_DIR}/app/tests/include/firebase/app_for_testing.h + ${FIREBASE_SOURCE_DIR}/app/src/fake/FIRApp.mm + ${FIREBASE_SOURCE_DIR}/app/src/fake/FIROptions.mm + ) + + target_include_directories(firebase_app_for_testing_ios + PUBLIC + ${FIREBASE_SOURCE_DIR}/app/src/fake + PRIVATE + ${FIREBASE_SOURCE_DIR} + ) + + target_link_libraries( + firebase_app_for_testing_ios + PRIVATE + "-framework UIKit" + "-framework Foundation" + ) + set_target_properties( + firebase_app_for_testing_ios PROPERTIES + MACOSX_BUNDLE_INFO_PLIST + ${TEST_RUNNER_DIR}/Info.plist + RESOURCE + ${TEST_RUNNER_DIR}/Info.plist + ) +else() + set(rest_mocks_SRCS + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/rest/transport_mock.h + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/rest/transport_mock.cc) + + add_library(firebase_rest_mocks STATIC + ${rest_mocks_SRCS}) + target_include_directories(firebase_rest_mocks + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_GEN_FILE_DIR} + ) + target_link_libraries(firebase_rest_mocks + PRIVATE + firebase_rest_lib + firebase_testing + ) +endif() + +firebase_cpp_cc_test_on_ios(firebase_app_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/app_test.cc + HOST + firebase_app_for_testing_ios + DEPENDS + firebase_app_for_testing + firebase_app + firebase_testing + flatbuffers + CUSTOM_FRAMEWORKS + UIKit +) + +firebase_cpp_cc_test(firebase_app_log_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/log_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test_on_ios(firebase_app_log_test + HOST + firebase_app_for_testing_ios + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/log_test.cc + DEPENDS + firebase_app + firebase_app_for_testing + firebase_testing +) + +firebase_cpp_cc_test(firebase_app_logger_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/logger_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_semaphore_test + SOURCES + ${FIREBASE_SOURCE_DIR}/app/tests/semaphore_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_assert_test + SOURCES + assert_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_assert_release_test + SOURCES + assert_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_optional_test + SOURCES + optional_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_cleanup_notifier_test + SOURCES + cleanup_notifier_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_cpp11_thread_test + SOURCES + thread_test.cc + DEPENDS + firebase_app + gtest +) + +firebase_cpp_cc_test(firebase_app_pthread_thread_test + SOURCES + thread_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_time_tests + SOURCES + time_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_scheduler_test + SOURCES + scheduler_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_path_test + SOURCES + path_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_locale_test + SOURCES + locale_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_callback_test + SOURCES + callback_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_reference_count_test + SOURCES + reference_count_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_uuid_test + SOURCES + uuid_test.cc + DEPENDS + firebase_app + +) + +firebase_cpp_cc_test(firebase_app_variant_test + SOURCES + variant_test.cc + DEPENDS + firebase_app +) + +add_library(flexbuffer_matcher + flexbuffer_matcher.cc +) + +target_include_directories(flexbuffer_matcher + PRIVATE + ${FIREBASE_SOURCE_DIR} + ${FLATBUFFERS_SOURCE_DIR}/include +) + +target_link_libraries(flexbuffer_matcher + PRIVATE + flatbuffers + firebase_testing + gmock + gtest +) + +firebase_cpp_cc_test(flexbuffer_matcher_test + SOURCES + flexbuffer_matcher_test.cc + DEPENDS + firebase_app + firebase_testing + flexbuffer_matcher + flatbuffers +) + +firebase_cpp_cc_test(firebase_app_variant_util_tests + SOURCES + variant_util_test.cc + DEPENDS + firebase_app + firebase_testing + flexbuffer_matcher + flatbuffers +) + +if (NOT IOS AND APPLE) + add_library(firebase_app_secure_darwin_testlib + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/secure/user_secure_darwin_internal_testlib.mm + ) + target_include_directories(firebase_app_secure_darwin_testlib + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ) + target_link_libraries(firebase_app_secure_darwin_testlib + PUBLIC + "-framework Foundation" + "-framework Security" + ) + set(platform_secure_testlib firebase_app_secure_darwin_testlib) +else() + set(platform_secure_testlib) +endif() + +firebase_cpp_cc_test(firebase_app_user_secure_manager_test + SOURCES + secure/user_secure_manager_test.cc + DEPENDS + firebase_app + ${platform_secure_testlib} + DEFINES + -DUSER_SECURE_LOCAL_TEST +) + +if(FIREBASE_FORCE_FAKE_SECURE_STORAGE) + set(SECURE_STORAGE_DEFINES + -DFORCE_FAKE_SECURE_STORAGE + ) +endif() + +firebase_cpp_cc_test(firebase_app_user_secure_integration_test + SOURCES + secure/user_secure_integration_test.cc + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/secure/user_secure_fake_internal.cc + DEPENDS + firebase_app + firebase_testing + ${platform_secure_testlib} + INCLUDES + ${LIBSECRET_INCLUDE_DIRS} + DEFINES + -DUSER_SECURE_LOCAL_TEST + ${SECURE_STORAGE_DEFINES} +) + +firebase_cpp_cc_test(firebase_app_user_secure_internal_test + SOURCES + secure/user_secure_internal_test.cc + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/secure/user_secure_fake_internal.cc + DEPENDS + firebase_app + firebase_testing + ${platform_secure_testlib} + INCLUDES + ${LIBSECRET_INCLUDE_DIRS} + DEFINES + -DUSER_SECURE_LOCAL_TEST + ${SECURE_STORAGE_DEFINES} +) + +firebase_cpp_cc_test(firebase_app_memory_atomic_test + SOURCES + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/memory/atomic_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_memory_shared_ptr_test + SOURCES + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/memory/shared_ptr_test.cc + DEPENDS + firebase_app +) + +firebase_cpp_cc_test(firebase_app_memory_unique_ptr_test + SOURCES + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/memory/unique_ptr_test.cc + DEPENDS + firebase_app +) + +#[[ google3 Dependencies + +# google3 - FLAGS_test_srcdir +firebase_cpp_cc_test(firebase_app_google_services_test + SOURCES + google_services_test.cc + INCLUDES + ${FIREBASE_GEN_FILE_DIR} + DEPENDS + flatbuffers +) + +# google3 - FLAGS_test_srcdir +firebase_cpp_cc_test(firebase_app_desktop_test + SOURCES + app_test.cc + DEPENDS + firebase_app + firebase_testing +) + +# google3 - openssl/base64.h +firebase_cpp_cc_test(firebase_app_base64_test + SOURCES + base64_openssh_test.cc + base64_test.cc + INCLUDES + ${OPENSSL_INCLUDE_DIR} + DEPENDS + firebase_app + ${OPENSSL_CRYPTO_LIBRARY} +) + +# google3 - thread/fiber/fiber.h (thread::Fiber) +firebase_cpp_cc_test(firebase_app_future_playbillingclient_test + SOURCES + future_playbillingclient_test.cc + DEPENDS + firebase_app +) + +# google3 - thread/fiber/fiber.h (thread::Fiber) +firebase_cpp_cc_test(firebase_app_future_test + SOURCES + future_test.cc + DEPENDS + firebase_app +) + +# google3 - thread/fiber/fiber.h (thread::Fiber) +firebase_cpp_cc_test(firebase_app_future_manager_test + SOURCES + future_manager_test.cc + DEPENDS + firebase_app +) + + +# google3 Dependencies ]] + diff --git a/app/tests/app_test.cc b/app/tests/app_test.cc new file mode 100644 index 0000000000..910a9a253c --- /dev/null +++ b/app/tests/app_test.cc @@ -0,0 +1,599 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#ifndef __ANDROID__ +#define __ANDROID__ +#endif // __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/app_common.h" +#include "app/src/app_identifier.h" +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/version.h" +#include "app/src/include/firebase/internal/platform.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include +#include +#include +#include + +#include "flatbuffers/util.h" + +#if defined(_WIN32) +#include +#define getcwd _getcwd +#define chdir _chdir +#else +#include +#endif // defined(_WIN32) + +#include "testing/config.h" +#include "testing/ticker.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif // __APPLE__ + +#if FIREBASE_PLATFORM_IOS +// Declared in the Obj-C header fake/FIRApp.h. +extern "C" { +void FIRAppCreateUsingDefaultOptions(const char* name); +void FIRAppResetApps(); +} +#endif // FIREBASE_PLATFORM_IOS + +// FLAGS_test_srcdir is not defined on Android and iOS so we can't read +// from test resources. +#if ((defined(__APPLE__) && TARGET_OS_IOS) || defined(__ANDROID__) || \ + defined(FIREBASE_ANDROID_FOR_DESKTOP)) +#define TEST_RESOURCES_AVAILABLE 0 +#else +#define TEST_RESOURCES_AVAILABLE 1 +#endif // MOBILE + +using testing::ContainsRegex; +using testing::HasSubstr; +using testing::Not; + +namespace firebase { + +class AppTest : public ::testing::Test { + protected: + AppTest() : current_path_buffer_(nullptr) { +#if TEST_RESOURCES_AVAILABLE + test_data_dir_ = + FLAGS_test_srcdir + "/google3/firebase/app/client/cpp/testdata"; + broken_test_data_dir_ = test_data_dir_ + "/broken"; +#endif // TEST_RESOURCES_AVAILABLE + } + + void SetUp() override { + SaveCurrentDirectory(); +#if TEST_RESOURCES_AVAILABLE + EXPECT_EQ(chdir(test_data_dir_.c_str()), 0); +#endif // TEST_RESOURCES_AVAILABLE + } + + void TearDown() override { + RestoreCurrentDirectory(); + ClearAppInstances(); + } + + // Create a mobile app instance using the fake options from resources. + void CreateMobileApp(const char* name) { +#if FIREBASE_PLATFORM_IOS + FIRAppCreateUsingDefaultOptions(name ? name : "__FIRAPP_DEFAULT"); +#endif // FIREBASE_PLATFORM_IOS +#if FIREBASE_ANDROID_FOR_DESKTOP + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + jclass firebase_app_class = + env->FindClass("com/google/firebase/FirebaseApp"); + env->ExceptionCheck(); + jclass firebase_options_class = + env->FindClass("com/google/firebase/FirebaseOptions"); + env->ExceptionCheck(); + jobject options = env->CallStaticObjectMethod( + firebase_options_class, + env->GetStaticMethodID( + firebase_options_class, "fromResource", + "(Landroid/content/Context;)" + "Lcom/google/firebase/FirebaseOptions;"), + firebase::testing::cppsdk::GetTestActivity()); + env->ExceptionCheck(); + jobject app_name = env->NewStringUTF(name ? name : "[DEFAULT]"); + jobject app = env->CallStaticObjectMethod( + firebase_app_class, + env->GetStaticMethodID( + firebase_app_class, "initializeApp", + "(Landroid/content/Context;" + "Lcom/google/firebase/FirebaseOptions;" + "Ljava/lang/String;)Lcom/google/firebase/FirebaseApp;"), + firebase::testing::cppsdk::GetTestActivity(), + options, + app_name); + env->ExceptionCheck(); + env->DeleteLocalRef(app); + env->DeleteLocalRef(app_name); + env->DeleteLocalRef(options); + env->DeleteLocalRef(firebase_options_class); + env->DeleteLocalRef(firebase_app_class); +#endif // FIREBASE_ANDROID_FOR_DESKTOP + } + + private: + // Clear all C++ firebase::App objects and any mobile SDK instances. + void ClearAppInstances() { + app_common::DestroyAllApps(); +#if FIREBASE_PLATFORM_IOS + FIRAppResetApps(); +#endif // FIREBASE_PLATFORM_IOS +#if FIREBASE_ANDROID_FOR_DESKTOP + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + jclass firebase_app_class = + env->FindClass("com/google/firebase/FirebaseApp"); + env->ExceptionCheck(); + env->CallStaticVoidMethod( + firebase_app_class, + env->GetStaticMethodID(firebase_app_class, "reset", "()V")); + env->ExceptionCheck(); + env->DeleteLocalRef(firebase_app_class); +#endif // FIREBASE_ANDROID_FOR_DESKTOP + } + + void SaveCurrentDirectory() { + assert(current_path_buffer_ == nullptr); + current_path_buffer_ = new char[FILENAME_MAX]; + getcwd(current_path_buffer_, FILENAME_MAX); + } + + void RestoreCurrentDirectory() { + assert(current_path_buffer_ != nullptr); + EXPECT_EQ(chdir(current_path_buffer_), 0); + delete[] current_path_buffer_; + current_path_buffer_ = nullptr; + } + + protected: + char* current_path_buffer_; + std::string test_data_dir_; + std::string broken_test_data_dir_; +}; + +// The following few tests are testing the setter and getter of AppOptions. + +TEST_F(AppTest, TestSetAppId) { + AppOptions options; + options.set_app_id("abc"); + EXPECT_STREQ("abc", options.app_id()); +} + +TEST_F(AppTest, TestSetApiKey) { + AppOptions options; + options.set_api_key("AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk"); + EXPECT_STREQ("AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk", options.api_key()); +} + +TEST_F(AppTest, TestSetMessagingSenderId) { + AppOptions options; + options.set_messaging_sender_id("012345678901"); + EXPECT_STREQ("012345678901", options.messaging_sender_id()); +} + +TEST_F(AppTest, TestSetDatabaseUrl) { + AppOptions options; + options.set_database_url("http://abc-xyz-123.firebaseio.com"); + EXPECT_STREQ("http://abc-xyz-123.firebaseio.com", options.database_url()); +} + +TEST_F(AppTest, TestSetGaTrackingId) { + AppOptions options; + options.set_ga_tracking_id("UA-12345678-1"); + EXPECT_STREQ("UA-12345678-1", options.ga_tracking_id()); +} + +TEST_F(AppTest, TestSetStorageBucket) { + AppOptions options; + options.set_storage_bucket("abc-xyz-123.storage.firebase.com"); + EXPECT_STREQ("abc-xyz-123.storage.firebase.com", options.storage_bucket()); +} + +TEST_F(AppTest, TestSetProjectId) { + AppOptions options; + options.set_project_id("myproject-123"); + EXPECT_STREQ("myproject-123", options.project_id()); +} + +TEST_F(AppTest, LoadDefault) { + AppOptions options; + EXPECT_EQ(&options, + AppOptions::LoadDefault( + &options +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); + EXPECT_STREQ("fake app id from resource", options.app_id()); + EXPECT_STREQ("fake api key from resource", options.api_key()); + EXPECT_STREQ("fake messaging sender id from resource", + options.messaging_sender_id()); + EXPECT_STREQ("fake database url from resource", options.database_url()); +#if FIREBASE_PLATFORM_IOS + // GA tracking ID can currently only be configured on iOS. + EXPECT_STREQ("fake ga tracking id from resource", options.ga_tracking_id()); +#endif // FIREBASE_PLATFORM_IOS + EXPECT_STREQ("fake storage bucket from resource", options.storage_bucket()); + EXPECT_STREQ("fake project id from resource", options.project_id()); +#if !FIREBASE_PLATFORM_IOS + // The application bundle ID isn't available in iOS tests. + EXPECT_STRNE("", options.package_name()); +#endif // !FIREBASE_PLATFORM_IOS +} + +TEST_F(AppTest, PopulateRequiredWithDefaults) { + AppOptions options; + EXPECT_STREQ("", options.app_id()); + EXPECT_STREQ("", options.api_key()); + EXPECT_STREQ("", options.project_id()); + EXPECT_TRUE( + options.PopulateRequiredWithDefaults( +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); + EXPECT_STREQ("fake app id from resource", options.app_id()); + EXPECT_STREQ("fake api key from resource", options.api_key()); + EXPECT_STREQ("fake project id from resource", options.project_id()); +} + +// The following tests create Firebase App instances. + +// Helper functions to create test instance. +std::unique_ptr CreateFirebaseApp() { + return std::unique_ptr(App::Create( +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +std::unique_ptr CreateFirebaseApp(const char* name) { + return std::unique_ptr(App::Create( + AppOptions(), name +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +std::unique_ptr CreateFirebaseApp(const AppOptions& options) { + return std::unique_ptr(App::Create( + options +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +std::unique_ptr CreateFirebaseApp(const AppOptions& options, + const char* name) { + return std::unique_ptr(App::Create( + options, + name +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + , + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + )); +} + +TEST_F(AppTest, TestCreateDefault) { + // Created with default options. + std::unique_ptr firebase_app = CreateFirebaseApp(); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ(firebase::kDefaultAppName, firebase_app->name()); +} + +TEST_F(AppTest, TestCreateDefaultWithExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp(nullptr); + // Create the C++ proxy object, since we've specified no options this should + // return a proxy to the previously created object. + std::unique_ptr firebase_app = CreateFirebaseApp(); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ(firebase::kDefaultAppName, firebase_app->name()); + // Make sure the options loaded from the fake resource are present. + EXPECT_STREQ("fake project id from resource", + firebase_app->options().project_id()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateNamedWithExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp("a named app"); + // Create the C++ proxy object, since we've specified no options this should + // return a proxy to the previously created object. + std::unique_ptr firebase_app = CreateFirebaseApp("a named app"); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("a named app", firebase_app->name()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateWithOptions) { + // Created with options as well as name. + std::unique_ptr firebase_app = CreateFirebaseApp("my_apps_name"); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("my_apps_name", firebase_app->name()); +} + +TEST_F(AppTest, TestCreateDefaultWithDifferentOptionsToExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp(nullptr); + // Create the C++ proxy object, this should delete the previously created + // object returning a new object with the specified options. + AppOptions options; + options.set_api_key("an api key"); + options.set_app_id("a different app id"); + options.set_project_id("a project id"); + std::unique_ptr firebase_app = CreateFirebaseApp(options); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("__FIRAPP_DEFAULT", firebase_app->name()); + EXPECT_STREQ("an api key", firebase_app->options().api_key()); + EXPECT_STREQ("a different app id", firebase_app->options().app_id()); + EXPECT_STREQ("a project id", firebase_app->options().project_id()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateNamedWithDifferentOptionsToExistingApp) { +#if FIREBASE_PLATFORM_MOBILE + // Create a default mobile app that will be proxied by a C++ app object. + CreateMobileApp("a named app"); + // Create the C++ proxy object, this should delete the previously created + // object returning a new object with the specified options. + AppOptions options; + options.set_api_key("an api key"); + options.set_app_id("a different app id"); + std::unique_ptr firebase_app = CreateFirebaseApp( + options, "a named app"); + EXPECT_NE(nullptr, firebase_app); + EXPECT_STREQ("a named app", firebase_app->name()); + EXPECT_STREQ("a different app id", firebase_app->options().app_id()); + EXPECT_STREQ("an api key", firebase_app->options().api_key()); +#endif // FIREBASE_PLATFORM_MOBILE +} + +TEST_F(AppTest, TestCreateMultipleTimes) { + // Created two apps with the same default name; the two are actually the same. + // We cannot use unique_ptr for this as the two will point to the same app. + App* firebase_app[2]; + for (int i = 0; i < 2; ++i) { + firebase_app[i] = App::Create( +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + ); + } + // There is only one app with the same name. + EXPECT_NE(nullptr, firebase_app[0]); + EXPECT_EQ(firebase_app[0], firebase_app[1]); + delete firebase_app[0]; +} + +// The following tests call GetInstance(). + +TEST_F(AppTest, TestGetDefaultInstance) { + // Nothing is created yet. We get nullptr. + EXPECT_EQ(nullptr, App::GetInstance()); + + // Now we create one. + std::unique_ptr firebase_app = CreateFirebaseApp(); + // We should get a non-nullptr pointer, which is what we created above. + EXPECT_NE(nullptr, App::GetInstance()); + EXPECT_EQ(firebase_app.get(), App::GetInstance()); + + // But there is one app for each distinct name. + EXPECT_EQ(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); +} + +TEST_F(AppTest, TestGetInstanceMultipleApps) { + // Nothing is created yet. + EXPECT_EQ(nullptr, App::GetInstance()); + EXPECT_EQ(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); + + // Now we create named app. + std::unique_ptr firebase_app = CreateFirebaseApp("thing_one"); + EXPECT_EQ(nullptr, App::GetInstance()); + EXPECT_NE(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(firebase_app.get(), App::GetInstance("thing_one")); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); + + // We again create a default app. + std::unique_ptr firebase_app_default = CreateFirebaseApp(); + EXPECT_NE(nullptr, App::GetInstance()); + EXPECT_EQ(firebase_app_default.get(), App::GetInstance()); + EXPECT_NE(nullptr, App::GetInstance("thing_one")); + EXPECT_EQ(firebase_app.get(), App::GetInstance("thing_one")); + EXPECT_NE(firebase_app, firebase_app_default); + EXPECT_EQ(nullptr, App::GetInstance("thing_two")); +} + +TEST_F(AppTest, TestParseUserAgent) { + app_common::RegisterLibrariesFromUserAgent("test/1 check/2 check/3"); + EXPECT_EQ(std::string(app_common::GetUserAgent()), + std::string("check/3 test/1")); +} + +TEST_F(AppTest, TestRegisterAndGetLibraryVersion) { + app_common::RegisterLibrary("a_library", "3.4.5"); + EXPECT_EQ("3.4.5", app_common::GetLibraryVersion("a_library")); + EXPECT_EQ("", app_common::GetLibraryVersion("a_non_existent_library")); +} + +TEST_F(AppTest, TestGetOuterMostSdkAndVersion) { + std::unique_ptr firebase_app_default = CreateFirebaseApp(); + std::string sdk; + std::string version; + app_common::GetOuterMostSdkAndVersion(&sdk, &version); + EXPECT_EQ(sdk, "fire-cpp"); + EXPECT_EQ(version, FIREBASE_VERSION_NUMBER_STRING); + app_common::RegisterLibrary("fire-mono", "4.5.6"); + app_common::GetOuterMostSdkAndVersion(&sdk, &version); + EXPECT_EQ(sdk, "fire-mono"); + EXPECT_EQ(version, "4.5.6"); + app_common::RegisterLibrary("fire-unity", "3.2.1"); + app_common::GetOuterMostSdkAndVersion(&sdk, &version); + EXPECT_EQ(sdk, "fire-unity"); + EXPECT_EQ(version, "3.2.1"); +} + +TEST_F(AppTest, TestRegisterLibrary) { + std::string firebase_version(std::string("fire-cpp/") + + std::string(FIREBASE_VERSION_NUMBER_STRING)); + std::unique_ptr firebase_app_default = CreateFirebaseApp(); + EXPECT_THAT(std::string(App::GetUserAgent()), HasSubstr(firebase_version)); + EXPECT_THAT(std::string(App::GetUserAgent()), + ContainsRegex("fire-cpp-os/(windows|darwin|linux|ios|android)")); + EXPECT_THAT(std::string(App::GetUserAgent()), + ContainsRegex("fire-cpp-arch/[^ ]+")); + EXPECT_THAT(std::string(App::GetUserAgent()), + ContainsRegex("fire-cpp-stl/[^ ]+")); + App::RegisterLibrary("fire-testing", "1.2.3"); + EXPECT_THAT(std::string(App::GetUserAgent()), + HasSubstr("fire-testing/1.2.3")); + firebase_app_default.reset(nullptr); + EXPECT_THAT(std::string(App::GetUserAgent()), + Not(HasSubstr("fire-testing/1.2.3"))); +} + +#if TEST_RESOURCES_AVAILABLE +TEST_F(AppTest, TestDefaultOptions) { + std::unique_ptr firebase_app = CreateFirebaseApp(AppOptions()); + + const AppOptions& options = firebase_app->options(); + EXPECT_STREQ("fake app id from resource", options.app_id()); + EXPECT_STREQ("fake api key from resource", options.api_key()); + EXPECT_STREQ("", options.messaging_sender_id()); + EXPECT_STREQ("", options.database_url()); + EXPECT_STREQ("", options.ga_tracking_id()); + EXPECT_STREQ("", options.storage_bucket()); + EXPECT_STREQ("fake project id from resource", options.project_id()); +} + +TEST_F(AppTest, TestReadOptionsFromResource) { + AppOptions app_options; + std::string json_file = test_data_dir_ + "/google-services.json"; + std::string config; + EXPECT_TRUE(flatbuffers::LoadFile(json_file.c_str(), false, &config)); + AppOptions::LoadFromJsonConfig(config.c_str(), &app_options); + std::unique_ptr firebase_app = CreateFirebaseApp(app_options); + + const AppOptions& options = firebase_app->options(); + // Check for the various fake options. + EXPECT_STREQ("fake mobilesdk app id", options.app_id()); + EXPECT_STREQ("fake api key", options.api_key()); + EXPECT_STREQ("fake project number", options.messaging_sender_id()); + EXPECT_STREQ("fake firebase url", options.database_url()); + // None of Firebase sample apps contain GA tracking_id. Looks like the field + // is either deprecated or not important. + EXPECT_STREQ("", options.ga_tracking_id()); + // Firebase auth sample app does not contain storage_bucket field. This could + // change and we should update here accordingly. + EXPECT_STREQ("", options.storage_bucket()); + EXPECT_STREQ("fake project id", options.project_id()); +} + +// Test that calling app.create() with no options tries to load from the local +// file google-services-desktop.json, before giving up. +TEST_F(AppTest, TestDefaultStart) { + // With no arguments, this will attempt to load a config from a file. + auto app = std::unique_ptr(App::Create()); + const AppOptions& options = app->options(); + EXPECT_STREQ(options.api_key(), "fake api key from resource"); + EXPECT_STREQ(options.storage_bucket(), "fake storage bucket from resource"); + EXPECT_STREQ(options.project_id(), "fake project id from resource"); + EXPECT_STREQ(options.database_url(), "fake database url from resource"); + EXPECT_STREQ(options.messaging_sender_id(), + "fake messaging sender id from resource"); +} + +TEST_F(AppTest, TestDefaultStartBrokenOptions) { + // Need to change the directory here to make sure we are in the same place + // as the broken google-services-desktop.json file. + EXPECT_EQ(chdir(broken_test_data_dir_.c_str()), 0); + // With no arguments, this will attempt to load a config from a file. + // This should fail as the file's format is invalid. + auto app = std::unique_ptr(App::Create()); + EXPECT_EQ(app.get(), nullptr); +} + +TEST_F(AppTest, TestCreateIdentifierFromOptions) { + { + AppOptions options; + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), ""); + } + { + AppOptions options; + options.set_package_name("org.foo.bar"); + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), + "org.foo.bar"); + } + { + AppOptions options; + options.set_project_id("cpp-sample-app-14e43"); + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), + "cpp-sample-app-14e43"); + } + { + AppOptions options; + options.set_project_id("cpp-sample-app-14e43"); + options.set_package_name("org.foo.bar"); + EXPECT_STREQ(internal::CreateAppIdentifierFromOptions(options).c_str(), + "org.foo.bar.cpp-sample-app-14e43"); + } +} + +#endif // TEST_RESOURCES_AVAILABLE +} // namespace firebase diff --git a/app/tests/assert_test.cc b/app/tests/assert_test.cc new file mode 100644 index 0000000000..fc4c6b416a --- /dev/null +++ b/app/tests/assert_test.cc @@ -0,0 +1,283 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/assert.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace { + +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::Ne; + +const char kTestMessage[] = "TEST_MESSAGE"; + +struct CallbackData { + LogLevel log_level; + std::string message; +}; + +void TestLogCallback(LogLevel log_level, const char* message, + void* callback_data) { + if (callback_data) { + auto* data = static_cast(callback_data); + data->log_level = log_level; + data->message = message; + } +} + +class AssertTest : public ::testing::Test { + public: + ~AssertTest() override { + LogSetCallback(nullptr, nullptr); + } +}; + +// Tests that check the functionality of FIREBASE_ASSERT_* macros in both debug +// and release builds. + +TEST_F(AssertTest, FirebaseAssertWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_WITH_EXPRESSION(false, FailureExpression), ""); +} + +TEST_F(AssertTest, FirebaseAssertAborts) { + EXPECT_DEATH(FIREBASE_ASSERT(false), ""); +} + +int FirebaseAssertReturnInt(int return_value) { + FIREBASE_ASSERT_RETURN(return_value, false); + return 0; +} + +TEST_F(AssertTest, FirebaseAssertReturnAborts) { + EXPECT_DEATH(FirebaseAssertReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseAssertReturnReturnsInt) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseAssertReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_RETURN_VOID(false), ""); +} + +void FirebaseAssertReturnVoid(int in_value, int* out_value) { + FIREBASE_ASSERT_RETURN_VOID(false); + *out_value = in_value; +} + +TEST_F(AssertTest, FirebaseAssertReturnVoidReturnsVoid) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseAssertMessageWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_MESSAGE_WITH_EXPRESSION( + false, FailureExpression, "Test Message: %s", kTestMessage), + ""); +} + +TEST_F(AssertTest, FirebaseAssertMessageAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_MESSAGE(false, "Test Message: %s", kTestMessage), + ""); +} + +int FirebaseAssertMessageReturnInt(int return_value) { + FIREBASE_ASSERT_MESSAGE_RETURN(return_value, false, "Test Message: %s", + kTestMessage); + return 0; +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnAborts) { + EXPECT_DEATH(FirebaseAssertMessageReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnReturnsInt) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertMessageReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_ASSERT_MESSAGE_RETURN_VOID(false, "Test Message: %s", + kTestMessage), + ""); +} + +void FirebaseAssertMessageReturnVoid(int in_value, int* out_value) { + FIREBASE_ASSERT_MESSAGE_RETURN_VOID(false, "Test Message: %s", kTestMessage); + *out_value = in_value; +} + +TEST_F(AssertTest, FirebaseAssertMessageReturnVoidReturnsVoid) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertMessageReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +#if !defined(NDEBUG) + +// Tests that check the functionality of FIREBASE_DEV_ASSERT_* macros in debug +// builds only. + +TEST_F(AssertTest, FirebaseDevAssertWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_WITH_EXPRESSION(false, FailureExpression), + ""); +} + +TEST_F(AssertTest, FirebaseDevAssertAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT(false), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnAborts) { + EXPECT_DEATH(FirebaseAssertReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnReturnsInt) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseDevAssertReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_RETURN_VOID(false), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnVoidReturnsVoid) { + auto* callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr("false")); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseDevAssertMessageWithExpressionAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_MESSAGE_WITH_EXPRESSION( + false, FailureExpression, "Test Message: %s", kTestMessage), + ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageAborts) { + EXPECT_DEATH( + FIREBASE_DEV_ASSERT_MESSAGE(false, "Test Message: %s", kTestMessage), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnAborts) { + EXPECT_DEATH(FirebaseAssertMessageReturnInt(1), ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnReturnsInt) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int return_value = 1; + EXPECT_THAT(FirebaseAssertMessageReturnInt(return_value), Eq(return_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnVoidAborts) { + EXPECT_DEATH(FIREBASE_DEV_ASSERT_MESSAGE_RETURN_VOID( + false, "Test Message: %s", kTestMessage), + ""); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnVoidReturnsVoid) { + auto callback_data = new CallbackData(); + LogSetCallback(TestLogCallback, callback_data); + int in_value = 1; + int out_value = 0; + FirebaseAssertMessageReturnVoid(in_value, &out_value); + EXPECT_THAT(out_value, Ne(in_value)); + EXPECT_THAT(callback_data->log_level, Eq(LogLevel::kLogLevelAssert)); + EXPECT_THAT(callback_data->message, HasSubstr(kTestMessage)); + delete callback_data; +} + +#else + +// Tests that check that FIREBASE_DEV_ASSERT_* macros are compiled out of +// release builds. + +TEST_F(AssertTest, FirebaseDevAssertWithExpressionCompiledOut) { + FIREBASE_DEV_ASSERT_WITH_EXPRESSION(false, FailureExpression); +} + +TEST_F(AssertTest, FirebaseDevAssertCompiledOut) { FIREBASE_DEV_ASSERT(false); } + +TEST_F(AssertTest, FirebaseDevAssertReturnCompiledOut) { + FIREBASE_DEV_ASSERT_RETURN(1, false); +} + +TEST_F(AssertTest, FirebaseDevAssertReturnVoidCompiledOut) { + FIREBASE_DEV_ASSERT_RETURN_VOID(false); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageWithExpressionCompiledOut) { + FIREBASE_DEV_ASSERT_MESSAGE_WITH_EXPRESSION(false, FailureExpression, + "Test Message: %s", kTestMessage); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageCompiledOut) { + FIREBASE_DEV_ASSERT_MESSAGE(false, "Test Message: %s", kTestMessage); +} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnCompiledOut){ + FIREBASE_DEV_ASSERT_MESSAGE_RETURN(1, false, "Test Message: %s", + kTestMessage)} + +TEST_F(AssertTest, FirebaseDevAssertMessageReturnVoidAborts) { + FIREBASE_DEV_ASSERT_MESSAGE_RETURN_VOID(false, "Test Message: %s", + kTestMessage); +} + +#endif // !defined(NDEBUG) + +} // namespace +} // namespace firebase diff --git a/app/tests/base64_openssh_test.cc b/app/tests/base64_openssh_test.cc new file mode 100644 index 0000000000..b415c6f92d --- /dev/null +++ b/app/tests/base64_openssh_test.cc @@ -0,0 +1,98 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/base64.h" +#include "app/src/log.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "openssl/base64.h" + +namespace firebase { +namespace internal { + +size_t OpenSSHEncodedLength(size_t input_size) { + size_t length; + if (!EVP_EncodedLength(&length, input_size)) { + return 0; + } + return length; +} + +bool OpenSSHEncode(const std::string& input, std::string* output) { + size_t base64_length = OpenSSHEncodedLength(input.size()); + output->resize(base64_length); + if (EVP_EncodeBlock(reinterpret_cast(&(*output)[0]), + reinterpret_cast(&input[0]), + input.size()) == 0u) { + return false; + } + // Trim the terminating null character. + output->resize(base64_length - 1); + return true; +} + +size_t OpenSSHDecodedLength(size_t input_size) { + size_t length; + if (!EVP_DecodedLength(&length, input_size)) { + return 0; + } + return length; +} + +bool OpenSSHDecode(const std::string& input, std::string* output) { + size_t decoded_length = OpenSSHDecodedLength(input.size()); + output->resize(decoded_length); + if (EVP_DecodeBase64(reinterpret_cast(&(*output)[0]), + &decoded_length, decoded_length, + reinterpret_cast(&(input)[0]), + input.size()) == 0) { + return false; + } + // Decoded length includes null termination, remove. + output->resize(decoded_length); + return true; +} + +TEST(Base64TestAgainstOpenSSH, TestEncodingAgainstOpenSSH) { + // Run this test 100 times. + for (int i = 0; i < 100; i++) { + // Generate 1-10000 random bytes. OpenSSH can't encode an empty string. + size_t bytes = 1 + rand() % 9999; // NOLINT + std::string orig; + orig.resize(bytes); + for (int c = 0; c < orig.size(); ++c) { + orig[c] = rand() % 0xFF; // NOLINT + } + + std::string encoded_firebase, encoded_openssh; + ASSERT_TRUE(Base64EncodeWithPadding(orig, &encoded_firebase)); + ASSERT_TRUE(OpenSSHEncode(orig, &encoded_openssh)); + EXPECT_EQ(encoded_firebase, encoded_openssh) + << "Encoding mismatch on source buffer: " << orig; + + std::string decoded_firebase_to_openssh; + std::string decoded_openssh_to_firebase; + ASSERT_TRUE(Base64Decode(encoded_openssh, &decoded_openssh_to_firebase)); + ASSERT_TRUE(OpenSSHDecode(encoded_firebase, &decoded_firebase_to_openssh)); + EXPECT_EQ(decoded_openssh_to_firebase, decoded_firebase_to_openssh) + << "Cross-decoding mismatch on source buffer: " << orig; + EXPECT_EQ(orig, decoded_firebase_to_openssh); + EXPECT_EQ(orig, decoded_openssh_to_firebase); + } +} + +} // namespace internal +} // namespace firebase diff --git a/app/tests/base64_test.cc b/app/tests/base64_test.cc new file mode 100644 index 0000000000..3e309a6e47 --- /dev/null +++ b/app/tests/base64_test.cc @@ -0,0 +1,221 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/base64.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace internal { + +TEST(Base64Test, EncodeAndDecodeText) { + // Test 3 different lengths of string to ensure that trailing = is handled + // correctly. + const std::string kOrig0("Hello, world!"), kEncoded0("SGVsbG8sIHdvcmxkIQ"); + const std::string kOrig1("How are you?"), kEncoded1("SG93IGFyZSB5b3U/"); + const std::string kOrig2("I'm fine..."), kEncoded2("SSdtIGZpbmUuLi4"); + + std::string encoded, decoded; + EXPECT_TRUE(Base64Encode(kOrig0, &encoded)); + EXPECT_EQ(encoded, kEncoded0); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig0); + + EXPECT_TRUE(Base64Encode(kOrig1, &encoded)); + EXPECT_EQ(encoded, kEncoded1); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig1); + + EXPECT_TRUE(Base64Encode(kOrig2, &encoded)); + EXPECT_EQ(encoded, kEncoded2); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig2); +} + +TEST(Base64Test, EncodeAndDecodeTextWithPadding) { + // Test 3 different lengths of string to ensure that trailing = is handled + // correctly. + const std::string kOrig0("Hello, world!"), kEncoded0("SGVsbG8sIHdvcmxkIQ=="); + const std::string kOrig1("How are you?"), kEncoded1("SG93IGFyZSB5b3U/"); + const std::string kOrig2("I'm fine..."), kEncoded2("SSdtIGZpbmUuLi4="); + + std::string encoded, decoded; + EXPECT_TRUE(Base64EncodeWithPadding(kOrig0, &encoded)); + EXPECT_EQ(encoded, kEncoded0); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig0); + + EXPECT_TRUE(Base64EncodeWithPadding(kOrig1, &encoded)); + EXPECT_EQ(encoded, kEncoded1); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig1); + + EXPECT_TRUE(Base64EncodeWithPadding(kOrig2, &encoded)); + EXPECT_EQ(encoded, kEncoded2); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kOrig2); +} + +TEST(Base64Test, SmallEncodeAndDecode) { + const std::string kEmpty; + std::string encoded, decoded; + EXPECT_TRUE(Base64Encode(kEmpty, &encoded)); + EXPECT_EQ(encoded, kEmpty); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode empty"; + EXPECT_EQ(decoded, kEmpty); + + EXPECT_TRUE(Base64EncodeWithPadding("\xFF", &encoded)); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, "\xFF"); + EXPECT_TRUE(Base64EncodeWithPadding("\xFF\xA0", &encoded)); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, "\xFF\xA0"); +} + +TEST(Base64Test, FullCharacterSet) { + // Ensure all 64 possible characters are properly parsed in all 4 positions. + const std::string kEncoded( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABC"); + std::string decoded, encoded; + EXPECT_TRUE(Base64Decode(kEncoded, &decoded)) + << "Couldn't decode " << kEncoded; + EXPECT_TRUE(Base64EncodeWithPadding(decoded, &encoded)); + EXPECT_EQ(encoded, kEncoded); +} + +TEST(Base64Test, BinaryEncodeAndDecode) { + // Check binary string. + const char kBinaryData[] = + "\x00\x05\x20\x3C\x40\x45\x50\x60\x70\x80\x90\x00\xA0\xB5\xC2\xD1\xF0" + "\xFF\x00\xE0\x42"; + + const std::string kBinaryOrig(kBinaryData, sizeof(kBinaryData) - 1); + const std::string kBinaryEncoded = "AAUgPEBFUGBwgJAAoLXC0fD/AOBC"; + std::string encoded, decoded; + + EXPECT_TRUE(Base64Encode(kBinaryOrig, &encoded)); + EXPECT_EQ(encoded, kBinaryEncoded); + EXPECT_TRUE(Base64Decode(encoded, &decoded)) << "Couldn't decode " << encoded; + EXPECT_EQ(decoded, kBinaryOrig); +} + +TEST(Base64Test, InPlaceEncodeAndDecode) { + const std::string kOrig("Hello, world!"), kEncoded("SGVsbG8sIHdvcmxkIQ"), + kEncodedWithPadding("SGVsbG8sIHdvcmxkIQ=="); + + // Ensure we can encode and decode in-place in the same buffer. + std::string buffer = kOrig; + EXPECT_TRUE(Base64Encode(buffer, &buffer)); + EXPECT_EQ(buffer, kEncoded); + EXPECT_TRUE(Base64Decode(buffer, &buffer)); + EXPECT_EQ(buffer, kOrig); + EXPECT_TRUE(Base64EncodeWithPadding(buffer, &buffer)); + EXPECT_EQ(buffer, kEncodedWithPadding); + EXPECT_TRUE(Base64Decode(buffer, &buffer)); + EXPECT_EQ(buffer, kOrig); +} + +TEST(Base64Test, FailToEncode) { + EXPECT_FALSE(Base64Encode("Hello", nullptr)); + EXPECT_FALSE(Base64EncodeWithPadding("Hello", nullptr)); +} + +TEST(Base64Test, FailToDecode) { + // Test some malformed base64. + std::string unused; + EXPECT_FALSE(Base64Decode("BadCharacterCountHere", &unused)); + EXPECT_FALSE(Base64Decode("HasEqual=SignInTheMiddle", &unused)); + EXPECT_FALSE(Base64Decode("EqualsFourFromEndA==AAAA", &unused)); + EXPECT_FALSE(Base64Decode("EqualsFourFromEndAA=AAAA", &unused)); + EXPECT_FALSE(Base64Decode("HasTooManyEqualsSignA===", &unused)); + EXPECT_FALSE(Base64Decode("PenultimateEqualsOnlyO=o", &unused)); + EXPECT_FALSE(Base64Decode("HasAnIncompatible$Symbol", &unused)); + + // Decoding should fail if there are any dangling '1' bits past the end of the + // encoded text. + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a==", &unused)); + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a", &unused)); + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a/=", &unused)); + EXPECT_FALSE(Base64Decode("ExtraLowBitsAtTheEnd0a/", &unused)); + + // Too short. + EXPECT_FALSE(Base64Decode("a", &unused)); + + // Test passing in nullptr as output. + EXPECT_FALSE(Base64Decode("abcd", nullptr)); +} + +TEST(Base64Test, TestSizeCalculations) { + EXPECT_EQ(GetBase64EncodedSize(""), 0); + EXPECT_EQ(GetBase64EncodedSize("a"), 4); + EXPECT_EQ(GetBase64EncodedSize("aa"), 4); + EXPECT_EQ(GetBase64EncodedSize("aaa"), 4); + EXPECT_EQ(GetBase64EncodedSize("aaaa"), 8); + EXPECT_EQ(GetBase64EncodedSize("aaaaa"), 8); + EXPECT_EQ(GetBase64EncodedSize("aaaaaa"), 8); + EXPECT_EQ(GetBase64EncodedSize("aaaaaaa"), 12); + + EXPECT_EQ(GetBase64DecodedSize(""), 0); + EXPECT_EQ(GetBase64DecodedSize("A"), 0); + EXPECT_EQ(GetBase64DecodedSize("AA"), 1); + EXPECT_EQ(GetBase64DecodedSize("AA=="), 1); + EXPECT_EQ(GetBase64DecodedSize("AAA"), 2); + EXPECT_EQ(GetBase64DecodedSize("AAA="), 2); + EXPECT_EQ(GetBase64DecodedSize("AAAA"), 3); + EXPECT_EQ(GetBase64DecodedSize("AAAAA"), 0); + EXPECT_EQ(GetBase64DecodedSize("AAAAAA"), 4); + EXPECT_EQ(GetBase64DecodedSize("AAAAAA=="), 4); + EXPECT_EQ(GetBase64DecodedSize("AAAAAAA"), 5); + EXPECT_EQ(GetBase64DecodedSize("AAAAAAA="), 5); + EXPECT_EQ(GetBase64DecodedSize("AAAAAAAA"), 6); +} + +TEST(Base64Test, TestUrlSafeEncoding) { + const std::string kEncoded( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABCAA"); + const std::string kEncodedUrlSafe( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ABCAA"); + const std::string kEncodedUrlSafeWithPadding( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_A" + "CDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_AB" + "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ABCAA=="); + std::string decoded, decoded_urlsafe; + EXPECT_TRUE(Base64Decode(kEncoded, &decoded)); + EXPECT_TRUE(Base64Decode(kEncodedUrlSafe, &decoded_urlsafe)); + EXPECT_EQ(decoded_urlsafe, decoded); + + std::string encoded_urlsafe; + EXPECT_TRUE(Base64EncodeUrlSafe(decoded, &encoded_urlsafe)); + EXPECT_EQ(encoded_urlsafe, kEncodedUrlSafe); + + std::string encoded_urlsafe_padded; + EXPECT_TRUE(Base64EncodeUrlSafeWithPadding(decoded, &encoded_urlsafe_padded)); + EXPECT_EQ(encoded_urlsafe_padded, kEncodedUrlSafeWithPadding); +} + +} // namespace internal +} // namespace firebase diff --git a/app/tests/callback_test.cc b/app/tests/callback_test.cc new file mode 100644 index 0000000000..f1175f966e --- /dev/null +++ b/app/tests/callback_test.cc @@ -0,0 +1,462 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/callback.h" + +#include +#include + +#include "app/memory/unique_ptr.h" +#include "app/src/mutex.h" +#include "app/src/thread.h" +#include "app/src/time.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; + +namespace firebase { + +class CallbackTest : public ::testing::Test { + protected: + CallbackTest() {} + + void SetUp() override { + callback_void_count_ = 0; + callback1_count_ = 0; + callback_value1_sum_ = 0; + callback_value1_ordered_.clear(); + callback_value2_sum_ = 0; + callback_string_.clear(); + value_and_string_ = std::pair(); + } + + // Counts callbacks from callback::CallbackVoid. + static void CountCallbackVoid() { callback_void_count_++; } + // Counts callbacks from callback::Callback1. + static void CountCallback1(void* test) { + CallbackTest* callback_test = *(static_cast(test)); + callback_test->callback1_count_++; + } + // Adds the value passed to CallbackValue1 to callback_value1_sum_. + static void SumCallbackValue1(int value) { callback_value1_sum_ += value; } + + // Add the value passed to CallbackValue1 to callback_value1_ordered_. + static void OrderedCallbackValue1(int value) { + callback_value1_ordered_.push_back(value); + } + + // Multiplies values passed to CallbackValue2 and adds them to + // callback_value2_sum_. + static void SumCallbackValue2(char value1, int value2) { + callback_value2_sum_ += value1 * value2; + } + + // Appends the string passed to this method to callback_string_. + static void AggregateCallbackString(const char* str) { + callback_string_ += str; + } + + // Stores this function's arguments in value_and_string_. + static void StoreValueAndString(int value, const char* str) { + value_and_string_ = std::pair(value, str); + } + + // Stores the value argument in value_and_string_.first, then appends the two + // string arguments and assign to value_and_string_.second, + static void StoreValueAndString2(const char* str1, const char* str2, + int value) { + value_and_string_ = std::pair( + value, std::string(str1) + std::string(str2)); + } + + // Stores the sum of value1 and value2 in value_and_string_.first and the + // string argumene in value_and_string_.second. + static void StoreValue2AndString(char value1, int value2, const char* str) { + value_and_string_ = std::pair(value1 + value2, str); + } + + // Adds the value passed to CallbackMoveValue1 to callback_value1_sum_. + static void SumCallbackMoveValue1(UniquePtr* value) { + callback_value1_sum_ += **value; + } + + int callback1_count_; + + static int callback_void_count_; + static int callback_value1_sum_; + static std::vector callback_value1_ordered_; + static int callback_value2_sum_; + static std::string callback_string_; + static std::pair value_and_string_; +}; + +int CallbackTest::callback_value1_sum_; +std::vector CallbackTest::callback_value1_ordered_; // NOLINT +int CallbackTest::callback_value2_sum_; +int CallbackTest::callback_void_count_; +std::string CallbackTest::callback_string_; // NOLINT +std::pair CallbackTest::value_and_string_; // NOLINT + +// Verify initialize and terminate setup and tear down correctly. +TEST_F(CallbackTest, TestInitializeAndTerminate) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::Initialize(); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::Terminate(false); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Verify Terminate() is a no-op if the API isn't initialized. +TEST_F(CallbackTest, TestTerminateWithoutInitialization) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::Terminate(false); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Add a callback to the queue then terminate the API. +TEST_F(CallbackTest, AddCallbackNoInitialization) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::Terminate(false); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Flush all callbacks. +TEST_F(CallbackTest, AddCallbacksTerminateAndFlush) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::PollCallbacks(); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::Terminate(true); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Add a callback to the queue, then remove it. This should result in +// initializing the callback API then tearing it down when the queue is empty. +TEST_F(CallbackTest, AddRemoveCallback) { + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + void* callback_reference = + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + callback::RemoveCallback(callback_reference); + callback::PollCallbacks(); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + EXPECT_THAT(callback_void_count_, Eq(0)); +} + +// Call a void callback. +TEST_F(CallbackTest, CallVoidCallback) { + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call two void callbacks. +TEST_F(CallbackTest, CallTwoVoidCallbacks) { + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call three void callbacks with a poll between them. +TEST_F(CallbackTest, CallOneVoidCallbackPollTwo) { + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::AddCallback(new callback::CallbackVoid(CountCallbackVoid)); + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(3)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call 2, 1 argument callbacks. +TEST_F(CallbackTest, CallCallback1) { + callback::AddCallback( + new callback::Callback1(this, CountCallback1)); + callback::AddCallback( + new callback::Callback1(this, CountCallback1)); + callback::PollCallbacks(); + EXPECT_THAT(callback1_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing the argument by value. +TEST_F(CallbackTest, CallCallbackValue1) { + callback::AddCallback( + new callback::CallbackValue1(10, SumCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(5, SumCallbackValue1)); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(15)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Ensure callbacks are executed in the order they're added to the queue. +TEST_F(CallbackTest, CallCallbackValue1Ordered) { + callback::AddCallback( + new callback::CallbackValue1(10, OrderedCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(5, OrderedCallbackValue1)); + callback::PollCallbacks(); + std::vector expected; + expected.push_back(10); + expected.push_back(5); + EXPECT_THAT(callback_value1_ordered_, Eq(expected)); +} + +// Schedule 3 callbacks, removing the middle one from the queue. +TEST_F(CallbackTest, ScheduleThreeCallbacksRemoveOne) { + callback::AddCallback( + new callback::CallbackValue1(1, SumCallbackValue1)); + void* reference = callback::AddCallback( + new callback::CallbackValue1(2, SumCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(4, SumCallbackValue1)); + callback::RemoveCallback(reference); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(5)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing two arguments by value. +TEST_F(CallbackTest, CallCallbackValue2) { + callback::AddCallback( + new callback::CallbackValue2(10, 4, SumCallbackValue2)); + callback::AddCallback( + new callback::CallbackValue2(20, 3, SumCallbackValue2)); + callback::PollCallbacks(); + EXPECT_THAT(callback_value2_sum_, Eq(100)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing a string by value. +TEST_F(CallbackTest, CallCallbackString) { + callback::AddCallback( + new callback::CallbackString("testing", AggregateCallbackString)); + callback::AddCallback( + new callback::CallbackString("123", AggregateCallbackString)); + callback::PollCallbacks(); + EXPECT_THAT(callback_string_, Eq("testing123")); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +// Call a callback passing a value and string by value. +TEST_F(CallbackTest, CallCallbackValue1String1) { + callback::AddCallback( + new callback::CallbackValue1String1(10, "ten", StoreValueAndString)); + callback::PollCallbacks(); + EXPECT_THAT(value_and_string_.first, Eq(10)); + EXPECT_THAT(value_and_string_.second, Eq("ten")); +} + +// Call a callback passing a value and two strings by value. +TEST_F(CallbackTest, CallCallbackString2Value1) { + callback::AddCallback(new callback::CallbackString2Value1( + "evening", "all", 11, StoreValueAndString2)); + callback::PollCallbacks(); + EXPECT_THAT(value_and_string_.first, Eq(11)); + EXPECT_THAT(value_and_string_.second, Eq("eveningall")); +} + +// Call a callback passing two values and a string by value. +TEST_F(CallbackTest, CallCallbackValue2String1) { + callback::AddCallback(new callback::CallbackValue2String1( + 11, 31, "meaning", StoreValue2AndString)); + callback::PollCallbacks(); + EXPECT_THAT(value_and_string_.first, Eq(42)); + EXPECT_THAT(value_and_string_.second, Eq("meaning")); +} + +// Call a callback passing the UniquePtr +TEST_F(CallbackTest, CallCallbackMoveValue1) { + callback::AddCallback(new callback::CallbackMoveValue1>( + MakeUnique(10), SumCallbackMoveValue1)); + UniquePtr ptr(new int(5)); + callback::AddCallback(new callback::CallbackMoveValue1>( + Move(ptr), SumCallbackMoveValue1)); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(15)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +#ifdef FIREBASE_USE_STD_FUNCTION +// Call a callback which wraps std::function +TEST_F(CallbackTest, CallCallbackStdFunction) { + int count = 0; + std::function callback = [&count]() { count++; }; + + callback::AddCallback(new callback::CallbackStdFunction(callback)); + callback::PollCallbacks(); + EXPECT_THAT(count, Eq(1)); + callback::AddCallback(new callback::CallbackStdFunction(callback)); + callback::AddCallback(new callback::CallbackStdFunction(callback)); + callback::PollCallbacks(); + EXPECT_THAT(count, Eq(3)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} +#endif + +// Ensure callbacks are executed in the order they're added to the queue with +// callbacks added to a different thread to the dispatching thread. +// Also, make sure it's possible to remove a callback from the queue while +// executing a callback. +TEST_F(CallbackTest, ThreadedCallbackValue1Ordered) { + bool running = true; + void* callback_entry_to_remove = nullptr; + Thread pollingThread( + [](void* arg) -> void { + volatile bool* running_ptr = static_cast(arg); + while (*running_ptr) { + callback::PollCallbacks(); + // Wait 20ms. + ::firebase::internal::Sleep(20); + } + }, + &running); + Thread addCallbacksThread( + [](void* arg) -> void { + void** callback_entry_to_remove_ptr = static_cast(arg); + callback::AddCallback( + new callback::CallbackValue1(1, OrderedCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(2, OrderedCallbackValue1)); + // Adds a callback which removes the entry referenced by + // callback_entry_to_remove. + callback::AddCallback(new callback::CallbackValue1( + callback_entry_to_remove_ptr, [](void** callback_entry) -> void { + callback::RemoveCallback(*callback_entry); + })); + *callback_entry_to_remove_ptr = callback::AddCallback( + new callback::CallbackValue1(4, OrderedCallbackValue1)); + callback::AddCallback( + new callback::CallbackValue1(5, OrderedCallbackValue1)); + }, + &callback_entry_to_remove); + addCallbacksThread.Join(); + callback::AddCallback(new callback::CallbackValue1( + &running, [](volatile bool* running_ptr) { *running_ptr = false; })); + pollingThread.Join(); + std::vector expected; + expected.push_back(1); + expected.push_back(2); + expected.push_back(5); + EXPECT_THAT(callback_value1_ordered_, Eq(expected)); +} + +TEST_F(CallbackTest, NewCallbackTest) { + callback::AddCallback(callback::NewCallback(SumCallbackValue1, 1)); + callback::AddCallback(callback::NewCallback(SumCallbackValue1, 2)); + callback::AddCallback( + callback::NewCallback(SumCallbackValue2, static_cast(1), 10)); + callback::AddCallback( + callback::NewCallback(SumCallbackValue2, static_cast(2), 100)); + callback::AddCallback( + callback::NewCallback(AggregateCallbackString, "Hello, ")); + callback::AddCallback( + callback::NewCallback(AggregateCallbackString, "World!")); + callback::PollCallbacks(); + EXPECT_THAT(callback_value1_sum_, Eq(3)); + EXPECT_THAT(callback_value2_sum_, Eq(210)); + EXPECT_THAT(callback_string_, testing::StrEq("Hello, World!")); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +TEST_F(CallbackTest, AddCallbackWithThreadCheckTest) { + // When PollCallbacks() is called in previous test, g_callback_thread_id + // would be set to current thread which runs the tests. We want it to be set + // to a different thread id in the beginning of this test. + Thread changeThreadIdThread([]() { + callback::AddCallback(new callback::CallbackVoid([](){})); + callback::PollCallbacks(); + }); + changeThreadIdThread.Join(); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + + void* entry_non_null = callback::AddCallbackWithThreadCheck( + new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_TRUE(entry_non_null != nullptr); + EXPECT_THAT(callback_void_count_, Eq(0)); + EXPECT_THAT(callback::IsInitialized(), Eq(true)); + + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(1)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + + // Once PollCallbacks() is called on this thread, AddCallbackWithThreadCheck() + // should run the callback immediately and return nullptr. + void* entry_null = callback::AddCallbackWithThreadCheck( + new callback::CallbackVoid(CountCallbackVoid)); + EXPECT_TRUE(entry_null == nullptr); + EXPECT_THAT(callback_void_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + + callback::PollCallbacks(); + EXPECT_THAT(callback_void_count_, Eq(2)); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); +} + +TEST_F(CallbackTest, CallbackDeadlockTest) { + // This is to test the deadlock scenario when CallbackEntry::Execute() and + // CallbackEntry::DisableCallback() are called at the same time. + // Ex. given a user mutex "user_mutex" + // GC thread: lock(user_mutex) -> lock(CallbackEntry::mutex_) + // Polling thread: lock(CallbackEntry::mutex_) -> lock(user_mutex) + // If both threads successfully obtain the first lock, a deadlock could occur. + // CallbackEntry::mutex_ should be released while running the callback. + + struct DeadlockData { + Mutex user_mutex; + void* handle; + }; + + for (int i = 0; i < 1000; ++i) { + DeadlockData data; + + data.handle = + callback::AddCallback(new callback::CallbackValue1( + &data, [](DeadlockData* data) { + MutexLock lock(data->user_mutex); + data->handle = nullptr; + })); + + Thread pollingThread([]() { callback::PollCallbacks(); }); + + Thread gcThread( + [](void* arg) { + DeadlockData* data = static_cast(arg); + MutexLock lock(data->user_mutex); + if (data->handle) { + callback::RemoveCallback(data->handle); + } + }, + &data); + + pollingThread.Join(); + gcThread.Join(); + EXPECT_THAT(callback::IsInitialized(), Eq(false)); + } +} +} // namespace firebase diff --git a/app/tests/cleanup_notifier_test.cc b/app/tests/cleanup_notifier_test.cc new file mode 100644 index 0000000000..52f7179cb9 --- /dev/null +++ b/app/tests/cleanup_notifier_test.cc @@ -0,0 +1,417 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/cleanup_notifier.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace testing { + +class CleanupNotifierTest : public ::testing::Test {}; + +namespace { +struct Object { + explicit Object(int counter_) : counter(counter_) {} + int counter; + + static void IncrementCounter(void* obj_void) { + reinterpret_cast(obj_void)->counter++; + } + static void DecrementCounter(void* obj_void) { + reinterpret_cast(obj_void)->counter--; + } +}; +} // namespace + +TEST_F(CleanupNotifierTest, TestCallbacksAreCalledAutomatically) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj, Object::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestCallbacksAreCalledManuallyOnceOnly) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj, Object::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + } + // Ensure the callback isn't called again. + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestCallbacksCanBeUnregistered) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj, Object::IncrementCounter); + cleanup.UnregisterObject(&obj); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(CleanupNotifierTest, TestMultipleObjects) { + Object obj1(1), obj2(2); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, Object::IncrementCounter); + cleanup.RegisterObject(&obj2, Object::IncrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(CleanupNotifierTest, TestMultipleCallbacksMultipleObjects) { + Object obj1(1), obj2(2); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, Object::IncrementCounter); + cleanup.RegisterObject(&obj2, Object::DecrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestOnlyOneCallbackPerObject) { + Object obj1(1), obj2(2); + { + CleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, Object::IncrementCounter); + cleanup.RegisterObject(&obj2, Object::IncrementCounter); + // The following call overwrites the previous callback on obj1: + cleanup.RegisterObject(&obj1, Object::DecrementCounter); + EXPECT_EQ(obj1.counter, 1); // Has not run. + } + EXPECT_EQ(obj1.counter, 0); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(CleanupNotifierTest, TestDoesNotCrashWhenYouUnregisterInvalidObject) { + Object obj(0); + { + CleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + } + EXPECT_EQ(obj.counter, 0); + { + CleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + cleanup.RegisterObject(&obj, Object::IncrementCounter); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(CleanupNotifierTest, TestDoesNotCrashIfCallingZeroCallbacks) { + Object obj(0); + { CleanupNotifier cleanup; } + { + CleanupNotifier cleanup; + cleanup.CleanupAll(); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(CleanupNotifierTest, TestMultipleCleanupNotifiersReferringToSameObject) { + Object obj(0); + { + CleanupNotifier cleanup1, cleanup2; + cleanup1.RegisterObject(&obj, Object::IncrementCounter); + cleanup2.RegisterObject(&obj, Object::IncrementCounter); + } + EXPECT_EQ(obj.counter, 2); +} + +namespace { +class OwnerObject { + public: + OwnerObject() { notifier_.RegisterOwner(this); } + ~OwnerObject() { notifier_.CleanupAll(); } + + protected: + CleanupNotifier notifier_; +}; + +class DerivedOwnerObject : public OwnerObject { + public: + DerivedOwnerObject() { notifier_.RegisterOwner(this); } + ~DerivedOwnerObject() {} +}; + +class SubscriberObject { + public: + SubscriberObject(void* subscribe_for_cleanup_object, + bool* flag_to_set_on_cleanup) + : subscribe_for_cleanup_object_(subscribe_for_cleanup_object), + flag_to_set_on_cleanup_(flag_to_set_on_cleanup) { + CleanupNotifier* notifier = + CleanupNotifier::FindByOwner(subscribe_for_cleanup_object_); + EXPECT_TRUE(notifier != nullptr); + notifier->RegisterObject(this, [](void* object) { + delete reinterpret_cast(object); + }); + } + + ~SubscriberObject() { + CleanupNotifier* notifier = + CleanupNotifier::FindByOwner(subscribe_for_cleanup_object_); + EXPECT_TRUE(notifier != nullptr); + notifier->UnregisterObject(this); + *flag_to_set_on_cleanup_ = true; + } + + private: + void* subscribe_for_cleanup_object_; + bool* flag_to_set_on_cleanup_; +}; +} // namespace + +class CleanupNotifierOwnerRegistryTest : public ::testing::Test {}; + +// Validate registration and retrieval of owner objects. +TEST_F(CleanupNotifierOwnerRegistryTest, RegisterAndFindByOwner) { + int owner1 = 1; + int owner2 = 2; + int owner3 = 3; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner3)); + { + CleanupNotifier notifier1; + { + CleanupNotifier notifier2; + notifier1.RegisterOwner(&owner1); + notifier1.RegisterOwner(&owner2); + notifier2.RegisterOwner(&owner2); + notifier2.RegisterOwner(&owner3); + EXPECT_EQ(¬ifier1, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(¬ifier2, CleanupNotifier::FindByOwner(&owner3)); + // Registration with notifier2 overrides owner2 association with + // notifier1. + EXPECT_EQ(¬ifier2, CleanupNotifier::FindByOwner(&owner2)); + } + EXPECT_EQ(¬ifier1, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner3)); + } + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, RegisterAndUnregisterByOwner) { + int owner1 = 1; + int owner2 = 2; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + { + CleanupNotifier notifier; + notifier.RegisterOwner(&owner1); + notifier.RegisterOwner(&owner2); + EXPECT_EQ(¬ifier, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(¬ifier, CleanupNotifier::FindByOwner(&owner2)); + notifier.UnregisterOwner(&owner2); + EXPECT_EQ(¬ifier, CleanupNotifier::FindByOwner(&owner1)); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner2)); + } + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(&owner1)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, CleanupRegistrationByOwnerObject) { + void* owner_pointer = nullptr; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); + Object cleanup_object(0); + { + OwnerObject owner; + owner_pointer = &owner; + // The cleanup notifier is not part of the public API of OwnerObject so we + // find it via a pointer to the object in the global registry. + CleanupNotifier* notifier = CleanupNotifier::FindByOwner(owner_pointer); + EXPECT_TRUE(notifier != nullptr); + notifier->RegisterObject(&cleanup_object, Object::IncrementCounter); + } + EXPECT_EQ(1, cleanup_object.counter); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, CleanupRegistrationByDerivedOwner) { + void* owner_pointer = nullptr; + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); + Object cleanup_object(0); + { + DerivedOwnerObject derived_owner; + owner_pointer = &derived_owner; + CleanupNotifier* notifier = CleanupNotifier::FindByOwner(owner_pointer); + EXPECT_TRUE(notifier != nullptr); + notifier->RegisterObject(&cleanup_object, Object::IncrementCounter); + } + EXPECT_EQ(1, cleanup_object.counter); + EXPECT_EQ(nullptr, CleanupNotifier::FindByOwner(owner_pointer)); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, + CleanupSubscriberObjectOnOwnerDeletion) { + bool subscriber_deleted = false; + OwnerObject* owner = new OwnerObject; + SubscriberObject* subscriber = + new SubscriberObject(owner, &subscriber_deleted); + (void)subscriber; + delete owner; + EXPECT_TRUE(subscriber_deleted); +} + +TEST_F(CleanupNotifierOwnerRegistryTest, + CleanupSubscriberObjectBeforeOwnerDeletion) { + bool subscriber_deleted = false; + OwnerObject owner; + { + SubscriberObject subscriber(&owner, &subscriber_deleted); + (void)subscriber; + } + (void)owner; + EXPECT_TRUE(subscriber_deleted); +} + +class TypedCleanupNotifierTest : public ::testing::Test {}; + +namespace { +struct TypedObject { + explicit TypedObject(int counter_) : counter(counter_) {} + int counter; + + static void IncrementCounter(TypedObject* obj) { obj->counter++; } + static void DecrementCounter(TypedObject* obj) { obj->counter--; } +}; +} // namespace + +TEST_F(TypedCleanupNotifierTest, TestCallbacksAreCalledAutomatically) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestCallbacksAreCalledManuallyOnceOnly) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + EXPECT_EQ(obj.counter, 0); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + cleanup.CleanupAll(); + EXPECT_EQ(obj.counter, 1); + } + // Ensure the callback isn't called again. + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestCallbacksCanBeUnregistered) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + cleanup.UnregisterObject(&obj); + EXPECT_EQ(obj.counter, 0); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(TypedCleanupNotifierTest, TestMultipleObjects) { + TypedObject obj1(1), obj2(2); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, TypedObject::IncrementCounter); + cleanup.RegisterObject(&obj2, TypedObject::IncrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(TypedCleanupNotifierTest, TestMultipleCallbacksMultipleObjects) { + TypedObject obj1(1), obj2(2); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, TypedObject::IncrementCounter); + cleanup.RegisterObject(&obj2, TypedObject::DecrementCounter); + } + EXPECT_EQ(obj1.counter, 2); + EXPECT_EQ(obj2.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestOnlyOneCallbackPerObject) { + TypedObject obj1(1), obj2(2); + { + TypedCleanupNotifier cleanup; + cleanup.RegisterObject(&obj1, TypedObject::IncrementCounter); + cleanup.RegisterObject(&obj2, TypedObject::IncrementCounter); + // The following call overwrites the previous callback on obj1: + cleanup.RegisterObject(&obj1, TypedObject::DecrementCounter); + EXPECT_EQ(obj1.counter, 1); // Has not run. + } + EXPECT_EQ(obj1.counter, 0); + EXPECT_EQ(obj2.counter, 3); +} + +TEST_F(TypedCleanupNotifierTest, + TestDoesNotCrashWhenYouUnregisterInvalidObject) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + } + EXPECT_EQ(obj.counter, 0); + { + TypedCleanupNotifier cleanup; + cleanup.UnregisterObject(&obj); // Should not crash. + cleanup.RegisterObject(&obj, TypedObject::IncrementCounter); + } + EXPECT_EQ(obj.counter, 1); +} + +TEST_F(TypedCleanupNotifierTest, TestDoesNotCrashIfCallingZeroCallbacks) { + TypedObject obj(0); + { TypedCleanupNotifier cleanup; } + { + TypedCleanupNotifier cleanup; + cleanup.CleanupAll(); + } + EXPECT_EQ(obj.counter, 0); +} + +TEST_F(TypedCleanupNotifierTest, + TestMultipleTypedCleanupNotifiersReferringToSameObject) { + TypedObject obj(0); + { + TypedCleanupNotifier cleanup1, cleanup2; + cleanup1.RegisterObject(&obj, TypedObject::IncrementCounter); + cleanup2.RegisterObject(&obj, TypedObject::IncrementCounter); + } + EXPECT_EQ(obj.counter, 2); +} + +} // namespace testing +} // namespace firebase diff --git a/app/tests/flexbuffer_matcher.cc b/app/tests/flexbuffer_matcher.cc new file mode 100644 index 0000000000..525186616d --- /dev/null +++ b/app/tests/flexbuffer_matcher.cc @@ -0,0 +1,252 @@ +// Copyright 2020 Google LLC +// +// 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 "app/tests/flexbuffer_matcher.h" + +// For testing purposes, we only care about the basic types. +enum FlexbuffersMetaTypes { + kNull, + kBool, + kInt, + kUInt, + kFloat, + kString, + kKey, + kMap, + kVector, + kBlob, +}; + +// Type names for error messages. +const char* meta_type_names[] = { + "Null", "Bool", "Int", "UInt", "Float", + "String", "Key", "Map", "Vector", "Blob", +}; + +FlexbuffersMetaTypes GetFlexbuffersReferenceType( + const flexbuffers::Reference& ref) { + switch (ref.GetType()) { + case flexbuffers::FBT_NULL: { + return kNull; + } + case flexbuffers::FBT_BOOL: { + return kBool; + } + case flexbuffers::FBT_INDIRECT_INT: + case flexbuffers::FBT_INT: { + return kInt; + } + case flexbuffers::FBT_INDIRECT_UINT: + case flexbuffers::FBT_UINT: { + return kUInt; + } + case flexbuffers::FBT_INDIRECT_FLOAT: + case flexbuffers::FBT_FLOAT: { + return kFloat; + } + case flexbuffers::FBT_KEY: { + return kKey; + } + case flexbuffers::FBT_STRING: { + return kString; + } + case flexbuffers::FBT_MAP: { + return kMap; + } + case flexbuffers::FBT_VECTOR: + case flexbuffers::FBT_VECTOR_INT: + case flexbuffers::FBT_VECTOR_UINT: + case flexbuffers::FBT_VECTOR_FLOAT: + case flexbuffers::FBT_VECTOR_KEY: + case flexbuffers::FBT_VECTOR_STRING_DEPRECATED: + case flexbuffers::FBT_VECTOR_INT2: + case flexbuffers::FBT_VECTOR_UINT2: + case flexbuffers::FBT_VECTOR_FLOAT2: + case flexbuffers::FBT_VECTOR_INT3: + case flexbuffers::FBT_VECTOR_UINT3: + case flexbuffers::FBT_VECTOR_FLOAT3: + case flexbuffers::FBT_VECTOR_INT4: + case flexbuffers::FBT_VECTOR_UINT4: + case flexbuffers::FBT_VECTOR_FLOAT4: + case flexbuffers::FBT_VECTOR_BOOL: { + return kVector; + } + case flexbuffers::FBT_BLOB: { + return kBlob; + } + } +} + +template +void MismatchMessage(const std::string& title, const T& expected, const T& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + *result_listener << title << ": Expected " << expected; + if (!location.empty()) { + *result_listener << " at " << location; + } + *result_listener << ", got " << arg; +} + +// TODO(73494146): Check in EqualsFlexbuffer gmock matcher into the canonical +// Flatbuffer repository. +// Because pushing things to the Flatbuffers library is a multistep process, I'm +// including this for now so the tests can be built. Once this has been merged +// into flatbuffers, we can remove this implementation of it and use the one +// supplied by Flatbuffers. +// +// Checks the equality of two Flexbuffers. This checker ignores whether values +// are 'Indirect' and typed vectors are treated as plain vectors. +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + FlexbuffersMetaTypes expected_type = GetFlexbuffersReferenceType(expected); + FlexbuffersMetaTypes arg_type = GetFlexbuffersReferenceType(arg); + + if (expected_type != arg_type) { + MismatchMessage("Type mismatch", meta_type_names[expected_type], + meta_type_names[arg_type], location, result_listener); + return false; + } + + if (expected.IsNull()) { + // No value checking necessary as Null has no value. + return true; + } + if (expected.IsBool()) { + if (expected.AsBool() != arg.AsBool()) { + MismatchMessage("Value mismatch", (expected.AsBool() ? "true" : "false"), + (arg.AsBool() ? "true" : "false"), location, + result_listener); + return false; + } + return true; + } else if (expected.IsInt()) { + if (expected.AsInt64() != arg.AsInt64()) { + MismatchMessage("Value mismatch", expected.AsInt64(), arg.AsInt64(), + location, result_listener); + return false; + } + return true; + } else if (expected.IsUInt()) { + if (expected.AsUInt64() != arg.AsUInt64()) { + MismatchMessage("Value mismatch", expected.AsUInt64(), arg.AsUInt64(), + location, result_listener); + return false; + } + return true; + } else if (expected.IsFloat()) { + if (expected.AsDouble() != arg.AsDouble()) { + MismatchMessage("Value mismatch", expected.AsDouble(), arg.AsDouble(), + location, result_listener); + return false; + } + return true; + } else if (expected.IsString()) { + if (strcmp(expected.AsString().c_str(), arg.AsString().c_str()) != 0) { + MismatchMessage("Value mismatch", expected.AsString().c_str(), + arg.AsString().c_str(), location, result_listener); + return false; + } + return true; + } else if (expected.IsKey()) { + if (strcmp(expected.AsKey(), arg.AsKey()) != 0) { + MismatchMessage("Key mismatch", expected.AsKey(), arg.AsKey(), location, + result_listener); + return false; + } + return true; + } else if (expected.IsBlob()) { + if (expected.AsBlob().size() != arg.AsBlob().size() | + std::memcmp(expected.AsBlob().data(), arg.AsBlob().data(), + expected.AsBlob().size()) != 0) { + *result_listener << "Binary mismatch"; + if (!location.empty()) { + *result_listener << " at " << location; + } + return false; + } + return true; + } else if (expected.IsMap()) { + flexbuffers::Map expected_map = expected.AsMap(); + flexbuffers::Map arg_map = arg.AsMap(); + if (expected_map.size() != arg_map.size()) { + MismatchMessage("Map size mismatch", + std::to_string(expected_map.size()) + " elements", + std::to_string(arg_map.size()) + " elements", location, + result_listener); + return false; + } + flexbuffers::TypedVector expected_keys = expected_map.Keys(); + flexbuffers::TypedVector arg_keys = arg_map.Keys(); + for (size_t i = 0; i < expected_keys.size(); ++i) { + std::string new_location = + location + "[" + expected_keys[i].AsKey() + "]"; + if (!EqualsFlexbufferImpl(expected_keys[i], arg_keys[i], new_location, + result_listener)) { + return false; + } + } + // Don't return in case of success, because we still need to check that + // the values match. This is done in the IsVector section, since Maps are + // also Vectors. + } + if (expected.IsVector()) { + flexbuffers::Vector expected_vector = expected.AsVector(); + flexbuffers::Vector arg_vector = arg.AsVector(); + if (expected_vector.size() != arg_vector.size()) { + MismatchMessage("Vector size mismatch", + std::to_string(expected_vector.size()) + " elements", + std::to_string(arg_vector.size()) + " elements", location, + result_listener); + return false; + } + for (size_t i = 0; i < expected_vector.size(); ++i) { + std::string new_location = location + "[" + std::to_string(i) + "]"; + if (!EqualsFlexbufferImpl(expected_vector[i], arg_vector[i], new_location, + result_listener)) { + return false; + } + } + return true; + } + *result_listener << "Unrecognized type"; + return false; +} + +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + return EqualsFlexbufferImpl(expected, flexbuffers::GetRoot(arg), location, + result_listener); +} + +bool EqualsFlexbufferImpl(const std::vector& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + return EqualsFlexbufferImpl(flexbuffers::GetRoot(expected), arg, location, + result_listener); +} + +bool EqualsFlexbufferImpl(const std::vector& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener) { + return EqualsFlexbufferImpl(flexbuffers::GetRoot(expected), + flexbuffers::GetRoot(arg), location, + result_listener); +} diff --git a/app/tests/flexbuffer_matcher.h b/app/tests/flexbuffer_matcher.h new file mode 100644 index 0000000000..a1cb98a5cb --- /dev/null +++ b/app/tests/flexbuffer_matcher.h @@ -0,0 +1,56 @@ +// Copyright 2020 Google LLC +// +// 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. + +#ifndef FIREBASE_APP_CLIENT_CPP_TESTS_FLEXBUFFER_MATCHER_H_ +#define FIREBASE_APP_CLIENT_CPP_TESTS_FLEXBUFFER_MATCHER_H_ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "flatbuffers/flexbuffers.h" + +// TODO(73494146): Check in EqualsFlexbuffer gmock matcher into the canonical +// Flatbuffer repository. +// Because pushing things to the Flatbuffers library is a multistep process, I'm +// including this for now so the tests can be built. Once this has been merged +// into flatbuffers, we can remove this implementation of it and use the one +// supplied by Flatbuffers. +// +// Checks the equality of two Flexbuffers. This checker ignores whether values +// are 'Indirect' and typed vectors are treated as plain vectors. +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +bool EqualsFlexbufferImpl(const flexbuffers::Reference& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +bool EqualsFlexbufferImpl(const std::vector& expected, + const flexbuffers::Reference& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +bool EqualsFlexbufferImpl(const std::vector& expected, + const std::vector& arg, + const std::string& location, + ::testing::MatchResultListener* result_listener); + +// TODO(73494146): Move this to Flabuffers. +MATCHER_P(EqualsFlexbuffer, expected, "") { + return EqualsFlexbufferImpl(expected, arg, "", result_listener); +} + +#endif // FIREBASE_APP_CLIENT_CPP_TESTS_FLEXBUFFER_MATCHER_H_ diff --git a/app/tests/flexbuffer_matcher_test.cc b/app/tests/flexbuffer_matcher_test.cc new file mode 100644 index 0000000000..044bef89e7 --- /dev/null +++ b/app/tests/flexbuffer_matcher_test.cc @@ -0,0 +1,236 @@ +// Copyright 2020 Google LLC +// +// 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 "app/tests/flexbuffer_matcher.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "flatbuffers/flexbuffers.h" + +using ::testing::Not; + +namespace { + +class FlexbufferMatcherTest : public ::testing::Test { + protected: + FlexbufferMatcherTest() : fbb_(512) {} + + void SetUp() override { + // Null type. + fbb_.Null(); + fbb_.Finish(); + null_flexbuffer_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Bool type. + fbb_.Bool(false); + fbb_.Finish(); + bool_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Bool(true); + fbb_.Finish(); + bool_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Int type. + fbb_.Int(5); + fbb_.Finish(); + int_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Int(10); + fbb_.Finish(); + int_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // UInt type. + fbb_.UInt(100); + fbb_.Finish(); + uint_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.UInt(500); + fbb_.Finish(); + uint_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Float type. + fbb_.Float(12.5); + fbb_.Finish(); + float_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Float(100.625); + fbb_.Finish(); + float_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // String type. + fbb_.String("A sailor went to sea sea sea"); + fbb_.Finish(); + string_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.String("To see what he could see see see"); + fbb_.Finish(); + string_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Key type. + fbb_.Key("But all that he could see see see"); + fbb_.Finish(); + key_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Key("Was the bottom of the deep blue sea sea sea"); + fbb_.Finish(); + key_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Map type. + fbb_.Map([&]() { + fbb_.Add("lorem", "ipsum"); + fbb_.Add("dolor", "sit"); + }); + fbb_.Finish(); + map_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Map([&]() { + fbb_.Add("amet", "consectetur"); + fbb_.Add("adipiscing", "elit"); + }); + fbb_.Finish(); + map_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Map([&]() { + fbb_.Add("sed", "do"); + fbb_.Add("eiusmod", "tempor"); + fbb_.Add("incididunt", "ut"); + }); + fbb_.Finish(); + map_flexbuffer_c_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Vector types. + fbb_.Vector([&]() { + fbb_ += "labore"; + fbb_ += "et"; + }); + fbb_.Finish(); + vector_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Vector([&]() { + fbb_ += "dolore"; + fbb_ += "magna"; + }); + fbb_.Finish(); + vector_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Vector([&]() { + fbb_ += "aliqua"; + fbb_ += "ut"; + fbb_ += "enim"; + }); + fbb_.Finish(); + vector_flexbuffer_c_ = fbb_.GetBuffer(); + fbb_.Clear(); + + // Blob types + fbb_.Blob("abcde", 5); + fbb_.Finish(); + blob_flexbuffer_a_ = fbb_.GetBuffer(); + fbb_.Clear(); + + fbb_.Blob("fghij", 5); + fbb_.Finish(); + blob_flexbuffer_b_ = fbb_.GetBuffer(); + fbb_.Clear(); + } + + flexbuffers::Builder fbb_; + std::vector null_flexbuffer_; + std::vector bool_flexbuffer_a_; + std::vector bool_flexbuffer_b_; + std::vector int_flexbuffer_a_; + std::vector int_flexbuffer_b_; + std::vector uint_flexbuffer_a_; + std::vector uint_flexbuffer_b_; + std::vector float_flexbuffer_a_; + std::vector float_flexbuffer_b_; + std::vector string_flexbuffer_a_; + std::vector string_flexbuffer_b_; + std::vector key_flexbuffer_a_; + std::vector key_flexbuffer_b_; + std::vector map_flexbuffer_a_; + std::vector map_flexbuffer_b_; + std::vector map_flexbuffer_c_; + std::vector vector_flexbuffer_a_; + std::vector vector_flexbuffer_b_; + std::vector vector_flexbuffer_c_; + std::vector blob_flexbuffer_a_; + std::vector blob_flexbuffer_b_; +}; + +// TODO(73494146): These tests should be moved to to the Flatbuffers repo whent +// the matcher itself is. +TEST_F(FlexbufferMatcherTest, IdentityChecking) { + EXPECT_THAT(null_flexbuffer_, EqualsFlexbuffer(null_flexbuffer_)); + EXPECT_THAT(bool_flexbuffer_a_, EqualsFlexbuffer(bool_flexbuffer_a_)); + EXPECT_THAT(int_flexbuffer_a_, EqualsFlexbuffer(int_flexbuffer_a_)); + EXPECT_THAT(uint_flexbuffer_a_, EqualsFlexbuffer(uint_flexbuffer_a_)); + EXPECT_THAT(float_flexbuffer_a_, EqualsFlexbuffer(float_flexbuffer_a_)); + EXPECT_THAT(string_flexbuffer_a_, EqualsFlexbuffer(string_flexbuffer_a_)); + EXPECT_THAT(key_flexbuffer_a_, EqualsFlexbuffer(key_flexbuffer_a_)); + EXPECT_THAT(map_flexbuffer_a_, EqualsFlexbuffer(map_flexbuffer_a_)); + EXPECT_THAT(vector_flexbuffer_a_, EqualsFlexbuffer(vector_flexbuffer_a_)); + EXPECT_THAT(blob_flexbuffer_a_, EqualsFlexbuffer(blob_flexbuffer_a_)); +} + +TEST_F(FlexbufferMatcherTest, TypeMismatch) { + EXPECT_THAT(null_flexbuffer_, Not(EqualsFlexbuffer(int_flexbuffer_b_))); + EXPECT_THAT(int_flexbuffer_a_, Not(EqualsFlexbuffer(uint_flexbuffer_b_))); + EXPECT_THAT(float_flexbuffer_a_, Not(EqualsFlexbuffer(bool_flexbuffer_b_))); + EXPECT_THAT(key_flexbuffer_a_, Not(EqualsFlexbuffer(string_flexbuffer_b_))); + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(vector_flexbuffer_b_))); +} + +TEST_F(FlexbufferMatcherTest, ValueMismatch) { + EXPECT_THAT(bool_flexbuffer_a_, Not(EqualsFlexbuffer(bool_flexbuffer_b_))); + EXPECT_THAT(int_flexbuffer_a_, Not(EqualsFlexbuffer(int_flexbuffer_b_))); + EXPECT_THAT(uint_flexbuffer_a_, Not(EqualsFlexbuffer(uint_flexbuffer_b_))); + EXPECT_THAT(float_flexbuffer_a_, Not(EqualsFlexbuffer(float_flexbuffer_b_))); + EXPECT_THAT(string_flexbuffer_a_, + Not(EqualsFlexbuffer(string_flexbuffer_b_))); + EXPECT_THAT(key_flexbuffer_a_, Not(EqualsFlexbuffer(key_flexbuffer_b_))); + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(map_flexbuffer_b_))); + EXPECT_THAT(vector_flexbuffer_a_, + Not(EqualsFlexbuffer(vector_flexbuffer_b_))); + EXPECT_THAT(blob_flexbuffer_a_, Not(EqualsFlexbuffer(blob_flexbuffer_b_))); +} + +TEST_F(FlexbufferMatcherTest, SizeMismatch) { + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(map_flexbuffer_c_))); + EXPECT_THAT(map_flexbuffer_a_, Not(EqualsFlexbuffer(map_flexbuffer_c_))); + EXPECT_THAT(vector_flexbuffer_a_, + Not(EqualsFlexbuffer(vector_flexbuffer_c_))); + EXPECT_THAT(vector_flexbuffer_a_, + Not(EqualsFlexbuffer(vector_flexbuffer_c_))); +} + +} // namespace diff --git a/app/tests/future_manager_test.cc b/app/tests/future_manager_test.cc new file mode 100644 index 0000000000..217f373ac1 --- /dev/null +++ b/app/tests/future_manager_test.cc @@ -0,0 +1,205 @@ +/* + * Copyright 2016 Google LLC + * + * 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 "app/src/future_manager.h" + +#include + +#include +#include + +#include "app/src/include/firebase/future.h" +#include "app/src/semaphore.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "thread/fiber/fiber.h" +#include "util/random/mt_random_thread_safe.h" + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Ne; +using ::testing::NotNull; + +namespace firebase { +namespace detail { +namespace testing { + +enum FutureManagerTestFn { kTestFnOne, kTestFnCount }; + +class FutureManagerTest : public ::testing::Test { + protected: + FutureManager future_manager_; + int value1_; + int value2_; + int value3_; +}; + +typedef FutureManagerTest FutureManagerDeathTest; + +TEST_F(FutureManagerTest, TestAllocFutureApis) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + future_manager_.AllocFutureApi(&value2_, kTestFnCount); + + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), + Ne(future_manager_.GetFutureApi(&value2_))); + EXPECT_THAT(future_manager_.GetFutureApi(&value3_), IsNull()); +} + +TEST_F(FutureManagerTest, TestMoveFutureApis) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + ReferenceCountedFutureImpl* impl = future_manager_.GetFutureApi(&value1_); + future_manager_.MoveFutureApi(&value1_, &value2_); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), NotNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), Eq(impl)); +} + +TEST_F(FutureManagerTest, TestReleaseFutureApi) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), NotNull()); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); +} + +TEST_F(FutureManagerTest, TestOrphaningFutures) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + EXPECT_THAT(future_impl, NotNull()); + + auto handle = future_impl->SafeAlloc(kTestFnOne); + Future future = + static_cast&>(future_impl->LastResult(kTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_impl->Complete(handle, 0, ""); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); +} + +TEST_F(FutureManagerDeathTest, TestCleanupOrphanedFuturesApis) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + EXPECT_THAT(future_impl, NotNull()); + + auto handle = future_impl->SafeAlloc(kTestFnOne); + handle.Detach(); + { + Future future = + static_cast&>(future_impl->LastResult(kTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + } + + // Future should still be valid even after cleanup since it is still pending. + future_manager_.CleanupOrphanedFutureApis(false); + EXPECT_THAT(future_impl->LastResult(kTestFnOne).status(), + Eq(kFutureStatusPending)); + + // Future should no longer be valid after cleanup since it is complete. + future_impl->Complete(handle, 0, ""); + future_manager_.CleanupOrphanedFutureApis(false); + EXPECT_DEATH(future_impl->SafeAlloc(kTestFnOne), "SIGSEGV"); +} + +TEST_F(FutureManagerDeathTest, TestCleanupOrphanedFuturesApisForcefully) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + EXPECT_THAT(future_impl, NotNull()); + + future_impl->SafeAlloc(kTestFnOne); + + { + Future future = + static_cast&>(future_impl->LastResult(kTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_manager_.ReleaseFutureApi(&value1_); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + } + + // Future should no longer be valid after force cleanup regardless of whether + // or not it is complete. + future_manager_.CleanupOrphanedFutureApis(true); + EXPECT_DEATH(future_impl->SafeAlloc(kTestFnOne), "SIGSEGV"); +} + +TEST_F(FutureManagerDeathTest, + TestCleanupIsNotTriggeredWhileRunningUserCallback) { + EXPECT_THAT(future_manager_.GetFutureApi(&value1_), IsNull()); + EXPECT_THAT(future_manager_.GetFutureApi(&value2_), IsNull()); + + future_manager_.AllocFutureApi(&value1_, kTestFnCount); + ReferenceCountedFutureImpl* future_impl = + future_manager_.GetFutureApi(&value1_); + // The other future api is only allocated so that it can be released in the + // completion, triggering cleanup. + future_manager_.AllocFutureApi(&value2_, kTestFnCount); + + auto handle = future_impl->SafeAlloc(kTestFnOne); + Future future(future_impl, handle.get()); + + Semaphore semaphore(0); + future.OnCompletion([&](const Future& future) { + // Triggers cleanup of orphaned instances (calls CleanupOrphanedFutureApis + // under the hood). + future_manager_.ReleaseFutureApi(&value2_); + // The future api shouldn't have been cleaned up by the previous line. + ASSERT_NE(future.status(), kFutureStatusInvalid); + EXPECT_EQ(*future.result(), 42); + + semaphore.Post(); + }); + + future_manager_.ReleaseFutureApi(&value1_); // Make it orphaned + // The future API, even though, orphaned, should not have been deallocated, + // because there is still a pending future associated with it. + EXPECT_EQ(future.status(), kFutureStatusPending); + future_impl->CompleteWithResult(handle, 0, "", 42); + + semaphore.Wait(); +} + +} // namespace testing +} // namespace detail +} // namespace firebase diff --git a/app/tests/future_test.cc b/app/tests/future_test.cc new file mode 100644 index 0000000000..080e080b0a --- /dev/null +++ b/app/tests/future_test.cc @@ -0,0 +1,1567 @@ +/* + * Copyright 2016 Google LLC + * + * 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 "app/src/include/firebase/future.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "app/src/reference_counted_future_impl.h" +#include "app/src/semaphore.h" +#include "app/src/thread.h" +#include "app/src/time.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Ne; +using ::testing::NotNull; + +// Namespace to use to access library components under test. +#if !defined(TEST_FIREBASE_NAMESPACE) +#define TEST_FIREBASE_NAMESPACE firebase +#endif // !defined(TEST_FIREBASE_NAMESPACE) + +namespace TEST_FIREBASE_NAMESPACE { +namespace detail { +namespace testing { + +struct TestResult { + int number; + std::string text; +}; + +class FutureTest : public ::testing::Test { + protected: + enum FutureTestFn { kFutureTestFnOne, kFutureTestFnTwo, kFutureTestFnCount }; + + FutureTest() : future_impl_(kFutureTestFnCount) {} + void SetUp() override { + handle_ = future_impl_.SafeAlloc(); + future_ = MakeFuture(&future_impl_, handle_); + } + + public: + ReferenceCountedFutureImpl future_impl_; + SafeFutureHandle handle_; + Future future_; +}; + +// Some arbitrary result and error values. +const int kResultNumber = 8675309; +const int kResultError = -1729; +const char* const kResultText = "Hello, world!"; + +// Check that a future can be completed by the same thread. +TEST_F(FutureTest, TestFutureCompletesInSameThread) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +static void FutureCallback(TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; +} + +// Check that the future completion can be done with a callback function +// instead of a lambda. +TEST_F(FutureTest, TestFutureCompletesWithCallback) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, FutureCallback); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that the LastResult() futures are properly set and completed. +TEST_F(FutureTest, TestLastResult) { + const auto handle = future_impl_.SafeAlloc(kFutureTestFnOne); + + Future future = static_cast&>( + future_impl_.LastResult(kFutureTestFnOne)); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle, 0); + + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); +} + +// Check that CompleteWithResult() works (i.e. data copy instead of lambda). +TEST_F(FutureTest, TestCompleteWithCopy) { + TestResult result; + result.number = kResultNumber; + result.text = kResultText; + future_impl_.CompleteWithResult(handle_, 0, result); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that Complete() with a lambda works. +TEST_F(FutureTest, TestCompleteWithLambda) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that Complete() with a lambda with a capture works. +TEST_F(FutureTest, TestCompleteWithLambdaCapture) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + bool captured = true; + future_impl_.Complete(handle_, 0, [&captured](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + captured = true; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + EXPECT_THAT(captured, Eq(true)); +} + +// Test that the result of a Pending future is null. +TEST_F(FutureTest, TestPendingResultIsNull) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_.result(), IsNull()); + EXPECT_THAT(future_.result_void(), IsNull()); +} + +// Check that a future can be completed from another thread. +TEST_F(FutureTest, TestFutureCompletesInAnotherThread) { + Thread child( + [](void* test_void) { + FutureTest* test = static_cast(test_void); + test->future_impl_.Complete(test->handle_, 0, + [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + }, + this); + child.Join(); // Blocks until the thread function is done + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Check that the future error can be set. +TEST_F(FutureTest, TestSettingErrorValue) { + future_impl_.Complete(handle_, kResultError); + EXPECT_THAT(future_.error(), Eq(kResultError)); +} + +// Check that the void and typed results match. +TEST_F(FutureTest, TestTypedAndVoidMatch) { + future_impl_.Complete(handle_, kResultError); + + EXPECT_THAT(future_.result(), NotNull()); + EXPECT_THAT(future_.result_void(), NotNull()); + EXPECT_THAT(future_.result(), Eq(future_.result_void())); +} + +TEST_F(FutureTest, TestReleasedBackingData) { + FutureHandleId id; + { + Future future; + { + SafeFutureHandle handle = + future_impl_.SafeAlloc(); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + id = handle.get().id(); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + future = MakeFuture(&future_impl_, handle); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + } + EXPECT_TRUE(future_impl_.ValidFuture(id)); + } + EXPECT_FALSE(future_impl_.ValidFuture(id)); +} + +TEST_F(FutureTest, TestDetachFutureHandle) { + FutureHandleId id; + { + Future future; + SafeFutureHandle handle = future_impl_.SafeAlloc(); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + id = handle.get().id(); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + future = MakeFuture(&future_impl_, handle); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + future = Future(); + EXPECT_TRUE(future_impl_.ValidFuture(handle)); + EXPECT_TRUE(future_impl_.ValidFuture(id)); + handle.Detach(); + EXPECT_FALSE(future_impl_.ValidFuture(handle)); + EXPECT_FALSE(future_impl_.ValidFuture(id)); + } + EXPECT_FALSE(future_impl_.ValidFuture(id)); +} + +// Test that a future becomes invalid when you release it. +TEST_F(FutureTest, TestReleasedFutureGoesInvalid) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + future_.Release(); + EXPECT_THAT(future_.status(), Eq(kFutureStatusInvalid)); +} + +// Test that an invalid future returns an error. +TEST_F(FutureTest, TestReleasedFutureHasError) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + future_.Release(); + EXPECT_THAT(future_.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_.error(), Ne(0)); +} + +TEST_F(FutureTest, TestCompleteSetsStatusToComplete) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); +} + +// Can't mock a simple function pointer, so we use these globals to ensure +// expectations about the callback running. +static int g_callback_times_called = -99; +static int g_callback_result_number = -99; +static void* g_callback_user_data = nullptr; + +// Test whether an OnCompletion callback is called when the future is completed +// with the templated version of Complete(). +TEST_F(FutureTest, TestCallbackCalledWhenSettingResult) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with the templated version of Complete(). +TEST_F(FutureTest, TestAddCallbackCalledWhenSettingResult) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an OnCompletion callback is called when the future is completed +// with a lambda with a capture. +TEST_F(FutureTest, TestCallbackCalledWithTypedLambdaCapture) { + int callback_times_called = 0; + int callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion([&](const Future& result) { + callback_times_called++; + callback_result_number = result.result()->number; + }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(callback_times_called, Eq(1)); + EXPECT_THAT(callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with a lambda with a capture. +TEST_F(FutureTest, TestAddCallbackCalledWithTypedLambdaCapture) { + int callback_times_called = 0; + int callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion([&](const Future& result) { + callback_times_called++; + callback_result_number = result.result()->number; + }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(callback_times_called, Eq(1)); + EXPECT_THAT(callback_result_number, Eq(kResultNumber)); +} + +// Test whether an OnCompletion callback is called when the future is completed +// with a lambda with a capture. +TEST_F(FutureTest, TestCallbackCalledWithBaseLambdaCapture) { + int callback_times_called = 0; + + // Set the callback before setting the status to complete. + static_cast(future_).OnCompletion( + [&](const FutureBase& result) { callback_times_called++; }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(callback_times_called, Eq(1)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with a lambda with a capture. +TEST_F(FutureTest, TestAddCallbackCalledWithBaseLambdaCapture) { + int callback_times_called = 0; + + // Set the callback before setting the status to complete. + static_cast(future_).AddOnCompletion( + [&](const FutureBase& result) { callback_times_called++; }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(callback_times_called, Eq(1)); +} + +void OnCompletionCallback(const Future& result, + void* /*user_data*/) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; +} + +// Test whether an OnCompletion callback is called when the callback is a +// function pointer instead of a lambda. +TEST_F(FutureTest, TestCallbackCalledWhenFunctionPointer) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion(OnCompletionCallback, nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called when the callback is a +// function pointer instead of a lambda. +TEST_F(FutureTest, TestAddCallbackCalledWhenFunctionPointer) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion(OnCompletionCallback, nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an OnCompletion callback is called when the future is completed +// with the non-templated version of Complete(). +TEST_F(FutureTest, TestCallbackCalledWhenNotSettingResults) { + g_callback_times_called = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); +} + +// Test whether an AddOnCompletion callback is called when the future is +// completed with the non-templated version of Complete(). +TEST_F(FutureTest, TestAddCallbackCalledWhenNotSettingResults) { + g_callback_times_called = 0; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); +} + +// Test whether an OnCompletion callback is called even if the future was +// already completed before OnCompletion() was called. +TEST_F(FutureTest, TestCallbackCalledWhenAlreadyComplete) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + // Callback should not be called until the callback is set. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + // Set the callback *after* the future was already completed. + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test whether an AddOnCompletion callback is called even if the future was +// already completed before AddOnCompletion() was set. +TEST_F(FutureTest, TestAddCallbackCalledWhenAlreadyComplete) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + // Callback should not be called until the callback is set. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + // Set the callback *after* the future was already completed. + future_.AddOnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestCallbackCalledFromAnotherThread) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + Thread child( + [](void* test_void) { + FutureTest* test = static_cast(test_void); + test->future_impl_.Complete( + test->handle_, 0, + [](TestResult* data) { data->number = kResultNumber; }); + }, + this); + + child.Join(); // Blocks until the thread function is done + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestAddCallbackCalledFromAnotherThread) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + future_.AddOnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + Thread child( + [](void* test_void) { + FutureTest* test = static_cast(test_void); + test->future_impl_.Complete( + test->handle_, 0, + [](TestResult* data) { data->number = kResultNumber; }); + }, + this); + + child.Join(); // Blocks until the fiber function is done + // Ensure the callback was still called. + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestCallbackUserData) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + future_.OnCompletion( + [](const Future&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestAddCallbackUserData) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + future_.AddOnCompletion( + [](const Future&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestCallbackUserDataFromBaseClass) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + static_cast(future_).OnCompletion( + [](const FutureBase&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestAddCallbackUserDataFromBaseClass) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + static_cast(future_).AddOnCompletion( + [](const FutureBase&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); +} + +TEST_F(FutureTest, TestUntypedCallback) { + g_callback_times_called = 0; + g_callback_result_number = 0; + static_cast(future_).OnCompletion( + [](const FutureBase& untyped_result, void*) { + g_callback_times_called++; + const Future& typed_result = + reinterpret_cast&>(untyped_result); + g_callback_result_number = typed_result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestAddUntypedCallback) { + g_callback_times_called = 0; + g_callback_result_number = 0; + static_cast(future_).AddOnCompletion( + [](const FutureBase& untyped_result, void*) { + g_callback_times_called++; + const Future& typed_result = + reinterpret_cast&>(untyped_result); + g_callback_result_number = typed_result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +// Test that you can deal with many simultaneous Futures at once. +TEST_F(FutureTest, TestSimultaneousFutures) { + const int kMilliseconds = 1000; + const int kNumToTest = 100; + + // Initialize a bunch of futures and threads. + std::vector> handles_; + std::vector> futures_; + std::vector children_; + struct Context { + FutureTest* test; + SafeFutureHandle handle; + int test_number; + } thread_context[kNumToTest]; + for (int i = 0; i < kNumToTest; i++) { + auto handle = future_impl_.SafeAlloc(); + handles_.push_back(handle); + futures_.push_back(MakeFuture(&future_impl_, handle)); + auto* context = &thread_context[i]; + context->test = this; + context->handle = handle; + context->test_number = i; + children_.push_back(new Thread( + [](void* current_context_void) { + Context* current_context = + static_cast(current_context_void); + // Each thread should wait a moment, then set the result and + // complete. + internal::Sleep(rand() % kMilliseconds); // NOLINT + current_context->test->future_impl_.Complete( + current_context->handle, 0, [current_context](TestResult* data) { + data->number = kResultNumber + current_context->test_number; + }); + }, + context)); + } + // Give threads time to run. + internal::Sleep(kMilliseconds); + + // Check that each future completed successfully, then clean it up. + for (int i = 0; i < kNumToTest; i++) { + children_[i]->Join(); + EXPECT_THAT(futures_[i].result()->number, Eq(kResultNumber + i)); + delete children_[i]; + children_[i] = nullptr; + } +} + +TEST_F(FutureTest, TestCallbackOnFutureOutOfScope) { + g_callback_times_called = 0; + g_callback_result_number = 0; + + // Set the callback before setting the status to complete. + future_.OnCompletion( + [](const Future& result, void*) { + g_callback_times_called++; + g_callback_result_number = result.result()->number; + }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_ = Future(); + handle_.Detach(); + // The Future we were holding onto is now out of scope. + + future_impl_.Complete( + handle_, 0, [](TestResult* data) { data->number = kResultNumber; }); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_result_number, Eq(kResultNumber)); +} + +TEST_F(FutureTest, TestOverridingHandle) { + // Ensure that FutureHandles can't be deallocated while still in use. + // Generally, do this by allocating a handle into a function slot, then + // allocating another handle into the same slot, and then creating a future + // from the first handle. If all goes well it should be fine, but if the + // handle was deallocated then making a future from it will fail. + + { + // Basic test, create 2 FutureHandles in the same slot, then make Future + // instances from both. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusPending); + } + { + // Same as above, but complete the first Future and make sure it doesn't + // affect the second. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + future_impl_.Complete( + handle1, 0, [](TestResult* data) { data->number = kResultNumber; }); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusComplete); + EXPECT_EQ(future1.result()->number, kResultNumber); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusPending); + } + { + // Complete the second Future and make sure it doesn't affect the first. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + future_impl_.Complete( + handle2, 0, [](TestResult* data) { data->number = kResultNumber; }); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusComplete); + EXPECT_EQ(future2.result()->number, kResultNumber); + } + { + // Ensure that both Futures can be completed with different result values. + SafeFutureHandle handle1 = + future_impl_.SafeAlloc(kFutureTestFnOne); + SafeFutureHandle handle2 = + future_impl_.SafeAlloc(kFutureTestFnOne); + future_impl_.Complete( + handle1, 0, [](TestResult* data) { data->number = kResultNumber; }); + future_impl_.Complete( + handle2, 0, [](TestResult* data) { data->number = 2 * kResultNumber; }); + Future future1 = MakeFuture(&future_impl_, handle1); + EXPECT_EQ(future1.status(), kFutureStatusComplete); + EXPECT_EQ(future1.result()->number, kResultNumber); + Future future2 = MakeFuture(&future_impl_, handle2); + EXPECT_EQ(future2.status(), kFutureStatusComplete); + EXPECT_EQ(future2.result()->number, 2 * kResultNumber); + } +} + +TEST_F(FutureTest, TestHighQps) { + const int kNumToTest = 10000; + + future_ = Future(); + + std::vector children_; + for (int i = 0; i < kNumToTest; i++) { + children_.push_back(new Thread( + [](void* this_void) { + FutureTest* this_ = reinterpret_cast(this_void); + SafeFutureHandle handle = + this_->future_impl_.SafeAlloc(kFutureTestFnOne); + + this_->future_impl_.Complete( + handle, 0, + [](TestResult* data) { data->number = kResultNumber; }); + Future future = MakeFuture(&this_->future_impl_, handle); + }, + this)); + } + for (int i = 0; i < kNumToTest; i++) { + children_[i]->Join(); + delete children_[i]; + children_[i] = nullptr; + } +} + +// Test that accessing a future as const compiles. +TEST_F(FutureTest, TestConstFuture) { + g_callback_times_called = 0; + + const Future const_future = future_; + // Set the callback before setting the status to complete. + const_future.OnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + const_future.AddOnCompletion([](const Future& result, + void*) { g_callback_times_called++; }, + nullptr); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(2)); +} + +// Test that we can remove an AddOnCompletion callback using RemoveOnCompletion. +TEST_F(FutureTest, TestAddCompletionCallbackRemoval) { + g_callback_times_called = 0; + auto callback_handle = future_.AddOnCompletion( + [&](const Future&) { ++g_callback_times_called; }); + future_.RemoveOnCompletion(callback_handle); + + future_impl_.Complete(handle_, 0); + + EXPECT_THAT(g_callback_times_called, Eq(0)); +} + +// Test that multiple callbacks are called in the documented order, +// and that OnCompletion() doesn't interfere with AddOnCompletion() +// and vice versa. +TEST_F(FutureTest, TestCallbackOrdering) { + std::vector ordered_results; + + // Set the callback before setting the status to complete. + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(5); }); + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(4); }); + auto callback_handle = future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(3); }); + future_.OnCompletion( + [&](const Future&) { ordered_results.push_back(-3); }); + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(2); }); + future_.OnCompletion( + [&](const Future&) { ordered_results.push_back(-2); }); + future_.OnCompletion( + [&](const Future&) { ordered_results.push_back(-1); }); + future_.AddOnCompletion( + [&](const Future&) { ordered_results.push_back(1); }); + future_.RemoveOnCompletion(callback_handle); + + // Callback should not be called until it is completed. + EXPECT_THAT(ordered_results, Eq(std::vector{})); + + future_impl_.Complete(handle_, 0); + + // The last OnCompletionCallback (-1) should get called before AddOnCompletion + // callbacks, and the AddOnCompletion callbacks should get called in + // the order that they were registered (5, 4, 3, 2, 1), except that callbacks + // which have been removed (3) should not be called. + EXPECT_THAT(ordered_results, Eq(std::vector{-1, 5, 4, 2, 1})); +} + +// Verify futures are not leaked when copied, using the implicit memory leak +// checker. When futures are allocated in the same LastResult function slot, a +// new handle should be allocated the old handle should be removed and hence be +// invalid. +TEST_F(FutureTest, VerifyNotLeakedWhenOverridden) { + FutureHandleId id; + { + SafeFutureHandle last_result_handle; + last_result_handle = future_impl_.SafeAlloc(0); + EXPECT_THAT(last_result_handle.get(), + Ne(SafeFutureHandle::kInvalidHandle.get())); + EXPECT_TRUE(future_impl_.ValidFuture(last_result_handle)); + id = last_result_handle.get().id(); + } + { + auto new_last_result_handle = future_impl_.SafeAlloc(0); + EXPECT_THAT(new_last_result_handle.get(), + Ne(SafeFutureHandle::kInvalidHandle.get())); + EXPECT_FALSE(future_impl_.ValidFuture(id)); + } +} + +// Verify that trying to complete a future twice causes death. +TEST_F(FutureTest, VerifyCompletingFutureTwiceAsserts) { + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + future_impl_.Complete(handle_, 0); + EXPECT_DEATH(future_impl_.Complete(handle_, 0), "SIGABRT"); +} + +// Verify that IsSafeToDelete() return the correct value. +TEST_F(FutureTest, VerifyIsSafeToDelete) { + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated but no external Future has ever + // reference it. + // Note: This will result in a warning message "Future with handle x still + // exists though its backing API y is being deleted" because there is no + // chance to remove the backing at all. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + auto handle_pending = impl.SafeAlloc(); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle_pending, 0); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated and an external Future has referenced + // it. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + auto handle_complete = impl.SafeAlloc(); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future* future = + new Future(&impl, handle_complete.get()); + EXPECT_FALSE(impl.IsSafeToDelete()); + delete future; + } + // This is true because ReferenceCountedFutureImpl::last_results_ never + // keeps a copy of this future. That is, the backing will be deleted when + // the future above is deleted. + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated with function id but no external + // Future has ever reference it. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + auto handle_fn_pending = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle_fn_pending, 0); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test if a FutureHandle is allocated with function id and an external Future + // has referenced it. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + auto handle_fn_complete = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future* future = + new Future(&impl, handle_fn_complete.get()); + EXPECT_FALSE(impl.IsSafeToDelete()); + delete future; + // This is false because ReferenceCountedFutureImpl::last_results_ keeps + // a copy of this future. + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle_fn_complete, 0); + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Test that a ReferenceCountedFutureImpl isn't considered for deletion while + // it's running a user callback. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + auto handle = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future future = MakeFuture(&impl, handle); + EXPECT_FALSE(impl.IsSafeToDelete()); + + Semaphore semaphore(0); + future.OnCompletion([&](const Future& future) { + EXPECT_FALSE(impl.IsSafeToDelete()); // Because the callback is running. + semaphore.Post(); + }); + future.Release(); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle, 0, ""); + + semaphore.Wait(); + + // Note: despite the semaphore, the check for `impl.IsSafeToDelete` is racy + // (it could be false if the check happens in-between when the semaphore + // posts the signal and when user callback actually finishes running), which + // necessitates sleeping. + const int kSleepTimeMs = 50; + int timeout_left = 1000; + while (!impl.IsSafeToDelete() && timeout_left >= 0) { + timeout_left -= kSleepTimeMs; + internal::Sleep(kSleepTimeMs); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } + + // Like the test above, but with AddOnCompletion instead of OnCompletion. + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + auto handle = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_FALSE(impl.IsSafeToDelete()); + Future future = MakeFuture(&impl, handle); + EXPECT_FALSE(impl.IsSafeToDelete()); + + Semaphore semaphore(0); + future.AddOnCompletion([&](const Future& future) { + EXPECT_FALSE(impl.IsSafeToDelete()); // Because the callback is running. + semaphore.Post(); + }); + future.Release(); + EXPECT_FALSE(impl.IsSafeToDelete()); + impl.Complete(handle, 0, ""); + + semaphore.Wait(); + + // Note: despite the semaphore, the check for `impl.IsSafeToDelete` is racy + // (it could be false if the check happens in-between when the semaphore + // posts the signal and when user callback actually finishes running), which + // necessitates sleeping. + const int kSleepTimeMs = 50; + int timeout_left = 1000; + while (!impl.IsSafeToDelete() && timeout_left >= 0) { + timeout_left -= kSleepTimeMs; + internal::Sleep(kSleepTimeMs); + } + EXPECT_TRUE(impl.IsSafeToDelete()); + } +} + +// Verify that IsReferencedExternally() returns the correct value. +TEST_F(FutureTest, VerifyIsReferencedExternally) { + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + EXPECT_FALSE(impl.IsReferencedExternally()); + } + + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + EXPECT_FALSE(impl.IsReferencedExternally()); + auto handle = impl.SafeAlloc(); + EXPECT_TRUE(impl.IsReferencedExternally()); + Future* future = new Future(&impl, handle.get()); + EXPECT_TRUE(impl.IsReferencedExternally()); + delete future; + } + EXPECT_FALSE(impl.IsReferencedExternally()); + } + + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + EXPECT_FALSE(impl.IsReferencedExternally()); + auto handle = impl.SafeAlloc(); + EXPECT_TRUE(impl.IsReferencedExternally()); + Future* future = new Future(&impl, handle.get()); + EXPECT_TRUE(impl.IsReferencedExternally()); + impl.Complete(handle, 0); + EXPECT_TRUE(impl.IsReferencedExternally()); + delete future; + } + EXPECT_FALSE(impl.IsReferencedExternally()); + } + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + { + EXPECT_FALSE(impl.IsReferencedExternally()); + auto handle = impl.SafeAlloc(kFutureTestFnOne); + EXPECT_TRUE(impl.IsReferencedExternally()); + { + Future* future = + new Future(&impl, handle.get()); + delete future; + } + EXPECT_TRUE(impl.IsReferencedExternally()); + handle.Detach(); + EXPECT_FALSE(impl.IsReferencedExternally()); + } + EXPECT_FALSE(impl.IsReferencedExternally()); + } +} + +// Verify that when a ReferenceCountedFutureImpl is deleted, any +// Futures it gave out are invalidated (rather than crashing). +TEST_F(FutureTest, VerifyFutureInvalidatedWhenImplIsDeleted) { + Future future_pending, future_complete, future_fn_pending, + future_fn_complete, future_invalid; + { + ReferenceCountedFutureImpl impl(kFutureTestFnCount); + // Allocate a variety of futures, completing some of them. + SafeFutureHandle handle_pending, handle_complete, + handle_fn_pending, handle_fn_complete; + + handle_pending = impl.SafeAlloc(); + future_pending = MakeFuture(&impl, handle_pending); + + handle_complete = impl.SafeAlloc(); + future_complete = MakeFuture(&impl, handle_complete); + impl.Complete(handle_complete, 0); + + handle_fn_pending = impl.SafeAlloc(kFutureTestFnOne); + future_fn_pending = MakeFuture(&impl, handle_fn_pending); + + handle_fn_complete = impl.SafeAlloc(kFutureTestFnTwo); + future_fn_complete = MakeFuture(&impl, handle_fn_complete); + impl.Complete(handle_fn_complete, 0); + + EXPECT_THAT(future_invalid.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_pending.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_complete.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_fn_pending.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_fn_complete.status(), Eq(kFutureStatusComplete)); + } + // Ensure that all different types/statuses of future are now invalid. + EXPECT_THAT(future_invalid.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_pending.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_complete.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_fn_pending.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future_fn_complete.status(), Eq(kFutureStatusInvalid)); +} + +// Verify that Future instances are cleaned up properly even if they've +// been copied and moved, even between FutureImpls, or released. +TEST_F(FutureTest, TestCleaningUpFuturesThatWereCopied) { + Future future1, future2, future3; + Future copy, move, release; + Future move_c, copy_c; // Constructor versions. + { + ReferenceCountedFutureImpl impl_a(kFutureTestFnCount); + { + ReferenceCountedFutureImpl impl_b(kFutureTestFnCount); + // Allocate a variety of futures, completing some of them. + SafeFutureHandle handle1, handle2, handle3; + + handle1 = impl_a.SafeAlloc(); + future1 = MakeFuture(&impl_a, handle1); + + handle2 = impl_a.SafeAlloc(); + future2 = MakeFuture(&impl_a, handle2); + + handle3 = impl_b.SafeAlloc(); + future3 = MakeFuture(&impl_b, handle3); + + EXPECT_THAT(future1.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future3.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(move.status(), Eq(kFutureStatusInvalid)); + + // Make some copies/moves. + copy = future3; + move = std::move(future3); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future3.status(), Eq(kFutureStatusInvalid)); // NOLINT + + future1 = copy; + future2 = move; // actually a copy + EXPECT_THAT(future1.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusPending)); + + release = copy; + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(release.status(), Eq(kFutureStatusPending)); + + release.Release(); + EXPECT_THAT(future1.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(release.status(), Eq(kFutureStatusInvalid)); + + // Ensure that the move/copy constructors also work. + Future move_constructor(std::move(move)); + Future copy_constructor(copy); + EXPECT_THAT(copy_constructor.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move_constructor.status(), Eq(kFutureStatusPending)); + + move_c = std::move(move_constructor); + copy_c = copy_constructor; + EXPECT_THAT(copy_c.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy_constructor.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(move_c.status(), Eq(kFutureStatusPending)); + } + // Ensure that all Futures are now invalid. + EXPECT_THAT(future1.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future2.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future3.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(copy.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(move.status(), Eq(kFutureStatusInvalid)); // NOLINT + EXPECT_THAT(copy_c.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(move_c.status(), Eq(kFutureStatusInvalid)); + } +} + +// Test Wait() method (without callback), with infinite timeout. +TEST_F(FutureTest, TestFutureWaitInfinite) { + Semaphore semaphore(0); + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + future_.Wait(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + child.Join(); // Clean up. +} + +// Test Wait() method with callback, with infinite timeout. +TEST_F(FutureTest, TestFutureWaitWithCallback) { + g_callback_times_called = 0; + g_callback_user_data = nullptr; + future_.OnCompletion( + [](const Future&, void* user_data) { + g_callback_times_called++; + g_callback_user_data = user_data; + }, + this); + + // Callback should not be called until it is completed. + EXPECT_THAT(g_callback_times_called, Eq(0)); + + Semaphore semaphore(0); + + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + future_.Wait(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + EXPECT_THAT(g_callback_times_called, Eq(1)); + EXPECT_THAT(g_callback_user_data, Eq(this)); + + child.Join(); // Clean up. +} + +// Test Wait() method with lambda callback. +TEST_F(FutureTest, TestFutureWaitWithCallbackLambda) { + int callback_times_called = 0; + future_.OnCompletion( + [&](const Future&) { callback_times_called++; }); + + // Callback should not be called until it is completed. + EXPECT_THAT(callback_times_called, Eq(0)); + + Semaphore semaphore(0); + + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + future_.Wait(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + EXPECT_THAT(callback_times_called, Eq(1)); + + child.Join(); // Clean up. +} + +// Test Await() method, with infinite timeout. +TEST_F(FutureTest, TestFutureAwait) { + Semaphore semaphore(0); + using This = decltype(this); + struct ThreadArgs { + This test_fixture; + Semaphore* semaphore; + } args{this, &semaphore}; + Thread child( + [](ThreadArgs* args_ptr) { + args_ptr->semaphore->Wait(); // Wait until main thread is ready. + args_ptr->test_fixture->future_impl_.Complete( + args_ptr->test_fixture->handle_, 0, [&](TestResult* data) { + internal::Sleep(/*milliseconds=*/100); + data->number = kResultNumber; + data->text = kResultText; + }); + }, + &args); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + semaphore.Post(); // Allow other thread to continue. + + const TestResult* result = future_.Await(FutureBase::kWaitTimeoutInfinite); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + ASSERT_THAT(future_.result(), Ne(nullptr)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + ASSERT_THAT(result, Ne(nullptr)); + EXPECT_THAT(result->number, Eq(kResultNumber)); + EXPECT_THAT(result->text, Eq(kResultText)); + + child.Join(); // Clean up. +} + +// Test Await() method, with finite timeout. +TEST_F(FutureTest, TestFutureTimedAwait) { + using This = decltype(this); + Thread child( + [](This test_fixture) { + internal::Sleep(/*milliseconds=*/300); + test_fixture->future_impl_.Complete( + test_fixture->handle_, 0, [](TestResult* data) { + data->number = kResultNumber; + data->text = kResultText; + }); + }, + this); + + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + EXPECT_THAT(future_.result(), Eq(nullptr)); + + const TestResult* result = future_.Await(100); // Wait for 100ms. + + // Thread should not have completed yet, for another 200ms... + EXPECT_THAT(result, Eq(nullptr)); + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + + result = future_.Await(500); // Wait for 500ms. + + // Thread should have completed by now. + ASSERT_THAT(result, Ne(nullptr)); + EXPECT_THAT(result->number, Eq(kResultNumber)); + EXPECT_THAT(result->text, Eq(kResultText)); + EXPECT_THAT(future_.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future_.result()->number, Eq(kResultNumber)); + EXPECT_THAT(future_.result()->text, Eq(kResultText)); + + child.Join(); // Clean up. +} + +// Helper functions to get memory usage. Linux only. +namespace { +extern "C" int get_memory_used_kb() { + int result = -1; +#ifdef __linux__ + FILE* file = fopen("/proc/self/status", "r"); + char line[128]; + + while (fgets(line, sizeof(line), file) != nullptr) { + if (strncmp(line, "VmSize:", 7) == 0) { + const char* nchar = &line[strlen(line) - 1]; + bool got_num = false; + while (nchar >= line) { + if (!got_num) { + if (isdigit(*nchar)) { + got_num = true; + } + } else { + if (!isdigit(*nchar)) { + result = atoi(nchar); // NOLINT + break; + } + } + nchar--; + } + } + } + fclose(file); +#endif // __linux__ + return result; +} +} // namespace + +TEST_F(FutureTest, MemoryStressTest) { + size_t kIterations = 4000000; // 4 million + + int memory_usage_before = get_memory_used_kb(); + for (size_t i = 0; i < kIterations; ++i) { + { + SafeFutureHandle handle = + i % 2 == 0 ? future_impl_.SafeAlloc() + : future_impl_.SafeAlloc(kFutureTestFnOne); + { + Future future = MakeFuture(&future_impl_, handle); + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + } + + if (i % 2 != 0) { + FutureBase future = future_impl_.LastResult(kFutureTestFnOne); + EXPECT_THAT(future_.status(), Eq(kFutureStatusPending)); + } + future_impl_.Complete(handle, 0, [i](TestResult* data) { + data->number = kResultNumber + i; + data->text = kResultText; + }); + { + Future future = MakeFuture(&future_impl_, handle); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.result()->number, Eq(kResultNumber + i)); + EXPECT_THAT(future.result()->text, Eq(kResultText)); + } + } + if (i % 2 != 0) { + FutureBase future_base = future_impl_.LastResult(kFutureTestFnOne); + Future& future = + *static_cast*>(&future_base); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.result()->number, Eq(kResultNumber + i)); + EXPECT_THAT(future.result()->text, Eq(kResultText)); + } + } + int memory_usage_after = get_memory_used_kb(); + + if (memory_usage_before != -1 && memory_usage_after != -1) { + // Ensure that after creating a few million futures, memory usage has not + // changed by more than half a megabyte. + const int kMaxAllowedMemoryChange = 512; // in kilobytes + EXPECT_NEAR(memory_usage_before, memory_usage_after, + kMaxAllowedMemoryChange); + } +} + +} // namespace testing +} // namespace detail +} // namespace TEST_FIREBASE_NAMESPACE diff --git a/app/tests/google_play_services/availability_android_test.cc b/app/tests/google_play_services/availability_android_test.cc new file mode 100644 index 0000000000..37119cf484 --- /dev/null +++ b/app/tests/google_play_services/availability_android_test.cc @@ -0,0 +1,242 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "absl/strings/str_format.h" + +#if !defined(__ANDROID__) +// We need enum definition in the header, which is only available for android. +// However, we cannot compile the entire test for android due to build error in +// portable //base library. +#define __ANDROID__ +#include "app/src/google_play_services/availability_android.h" +#undef __ANDROID__ +#endif // !defined(__ANDROID__) + +#include "base/stringprintf.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/run_all_tests.h" +#include "testing/testdata_config_generated.h" +#include "testing/ticker.h" + +namespace google_play_services { + +// Wait for a future up to the specified number of milliseconds. +template +static void WaitForFutureWithTimeout( + const firebase::Future& future, + int timeout_milliseconds = 1000 /* 1 second */, + firebase::FutureStatus expected_status = firebase::kFutureStatusComplete) { + while (future.status() != expected_status && timeout_milliseconds-- > 0) { + usleep(1000 /* microseconds per millisecond */); + } +} + +TEST(AvailabilityAndroidTest, Initialize) { + // Initialization should succeed. + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // Clean up afterwards. + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, InitializeTwice) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + // Should be fine if called again. + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // Terminate needs to be called twice to properly clean up. + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, CheckAvailabilityOther) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // Get null from getInstance(). Result is unavailable (other). + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailability.getInstance'}" + " ]" + "}"); + EXPECT_EQ(kAvailabilityUnavailableOther, + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + // We do not care about result 10 and specify it as other. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:10}}" + " ]" + "}"); + EXPECT_EQ(kAvailabilityUnavailableOther, + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, CheckAvailabilityCases) { + // Enums are defined in com.google.android.gms.common.ConnectionResult. + const int kTestData[] = { + 0, // SUCCESS + 1, // SERVICE_MISSING + 2, // SERVICE_VERSION_UPDATE_REQUIRED + 3, // SERVICE_DISABLED + 9, // SERVICE_INVALID + 18, // SERVICE_UPDATING + 19 // SERVICE_MISSING_PERMISSION + }; + const Availability kExpected[7] = {kAvailabilityAvailable, + kAvailabilityUnavailableMissing, + kAvailabilityUnavailableUpdateRequired, + kAvailabilityUnavailableDisabled, + kAvailabilityUnavailableInvalid, + kAvailabilityUnavailableUpdating, + kAvailabilityUnavailablePermissions}; + // Now test each of the specific status. + for (int i = 0; i < sizeof(kTestData) / sizeof(kTestData[0]); ++i) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + std::string testdata = absl::StrFormat( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:%d}}" + " ]" + "}", + kTestData[i]); + firebase::testing::cppsdk::ConfigSet(testdata.c_str()); + EXPECT_EQ(kExpected[i], + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); + } +} + +TEST(AvailabilityAndroidTest, CheckAvailabilityCached) { + const int kTestData[] = { + 0, // SUCCESS + 1, // SERVICE_MISSING + 2, // SERVICE_VERSION_UPDATE_REQUIRED + }; + const Availability kExpected = kAvailabilityAvailable; + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + for (int i = 0; i < sizeof(kTestData) / sizeof(kTestData[0]); ++i) { + std::string testdata = absl::StrFormat( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:%d}}" + " ]" + "}", + kTestData[i]); + firebase::testing::cppsdk::ConfigSet(testdata.c_str()); + EXPECT_EQ(kExpected, + CheckAvailability(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, MakeAvailableAlreadyAvailable) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + // Google play services are already available. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable'," + " futurebool:{value:True}, futureint:{value:0, ticker:0}}" + " ]" + "}"); + { + firebase::Future result = MakeAvailable( + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity()); + WaitForFutureWithTimeout(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(0, result.error()); + EXPECT_STREQ("result code is 0", result.error_message()); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, MakeAvailableFailed) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + // We cannot make Google play services available. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable'," + " futurebool:{value:False}, futureint:{value:0, ticker:-1}}" + " ]" + "}"); + { + firebase::Future result = MakeAvailable( + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity()); + WaitForFutureWithTimeout(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(-1, result.error()); + EXPECT_STREQ("Call to makeGooglePlayServicesAvailable failed.", + result.error_message()); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +TEST(AvailabilityAndroidTest, MakeAvailableWithStatus) { + EXPECT_TRUE(Initialize(firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity())); + firebase::testing::cppsdk::TickerReset(); + // We try to make Google play services available. The only difference between + // succeeded status and failed status is the result code. The logic is in the + // java helper code and transparent to the C++ code. So here we use an + // arbitrary status code 7 instead of testing each one by one. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailabilityHelper.makeGooglePlayServicesAvailable'," + " futurebool:{value:True}, futureint:{value:7, ticker:1}}" + " ]" + "}"); + { + firebase::Future result = MakeAvailable( + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity()); + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); + WaitForFutureWithTimeout(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(7, result.error()); + EXPECT_STREQ("result code is 7", result.error_message()); + } + Terminate(firebase::testing::cppsdk::GetTestJniEnv()); +} + +} // namespace google_play_services diff --git a/app/tests/google_services_test.cc b/app/tests/google_services_test.cc new file mode 100644 index 0000000000..4707278329 --- /dev/null +++ b/app/tests/google_services_test.cc @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/google_services_resource.h" +#include "app/src/log.h" +#include "flatbuffers/idl.h" +#include "flatbuffers/util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace fbs { + +// Helper function to parse config and return whether the config is valid. +bool Parse(const char* config) { + flatbuffers::IDLOptions options; + options.skip_unexpected_fields_in_json = true; + flatbuffers::Parser parser(options); + + // Parse schema. + const char* schema = + reinterpret_cast(google_services_resource_data); + if (!parser.Parse(schema)) { + ::firebase::LogError("Failed to parse schema: ", parser.error_.c_str()); + return false; + } + + // Parse actual config. + if (!parser.Parse(config)) { + ::firebase::LogError("Invalid JSON: ", parser.error_.c_str()); + return false; + } + + return true; +} + +// Test the conformity of the provided .json file. +TEST(GoogleServicesTest, TestConformity) { + // This is an actual .json, copied from Firebase auth sample app. + std::string json_file = + FLAGS_test_srcdir + + "/google3/firebase/app/client/cpp/testdata/google-services.json"; + std::string json_str; + EXPECT_TRUE(flatbuffers::LoadFile(json_file.c_str(), false, &json_str)); + EXPECT_FALSE(json_str.empty()); + EXPECT_TRUE(Parse(json_str.c_str())); +} + +// Sanity check to parse a non-conform config. +TEST(GoogleServicesTest, TestNonConformity) { + EXPECT_FALSE(Parse("{project_info:[1, 2, 3]}")); +} + +// Test that extra field in .json is ok. +TEST(GoogleServicesTest, TestExtraField) { + EXPECT_TRUE(Parse("{game_version:3.1415926}")); +} + +} // namespace fbs +} // namespace firebase diff --git a/app/tests/include/firebase/app_for_testing.h b/app/tests/include/firebase/app_for_testing.h new file mode 100644 index 0000000000..bf4e301cd0 --- /dev/null +++ b/app/tests/include/firebase/app_for_testing.h @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#ifndef FIREBASE_APP_CLIENT_CPP_TESTS_INCLUDE_FIREBASE_APP_FOR_TESTING_H_ +#define FIREBASE_APP_CLIENT_CPP_TESTS_INCLUDE_FIREBASE_APP_FOR_TESTING_H_ + +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "testing/run_all_tests.h" + +namespace firebase { +namespace testing { + +// Populate AppOptions with mock required values for testing. +static AppOptions MockAppOptions() { + AppOptions options; + options.set_app_id("com.google.firebase.testing"); + options.set_api_key("not_a_real_api_key"); + options.set_project_id("not_a_real_project_id"); + return options; +} + +// Create a named firebase::App with the specified options. +static App* CreateApp(const AppOptions& options, const char* name) { + return App::Create(options, name +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + , + // Additional parameters are required for Android. + firebase::testing::cppsdk::GetTestJniEnv(), + firebase::testing::cppsdk::GetTestActivity() +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + ); +} + +// Create a default firebase::App with the specified options. +static App* CreateApp(const AppOptions& options) { + return CreateApp(options, firebase::kDefaultAppName); +} + +// Create a firebase::App with mock options. +static App* CreateApp() { return CreateApp(MockAppOptions()); } + +} // namespace testing +} // namespace firebase + +#endif // FIREBASE_APP_CLIENT_CPP_TESTS_INCLUDE_FIREBASE_APP_FOR_TESTING_H_ diff --git a/app/tests/intrusive_list_test.cc b/app/tests/intrusive_list_test.cc new file mode 100644 index 0000000000..7abd361402 --- /dev/null +++ b/app/tests/intrusive_list_test.cc @@ -0,0 +1,1229 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// 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 "app/src/intrusive_list.h" + +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +// EXPECT_DEATH tests don't work on Android or Windows. +#if defined(__ANDROID__) || defined(_MSC_VER) +#define NO_DEATH_TESTS +#endif // __ANDROID__ + +class IntegerListNode { + public: + explicit IntegerListNode(int value) : node(), value_(value) {} + // Older versions of Visual Studio don't generate move constructors or move + // assignment operators. + IntegerListNode(IntegerListNode&& other) { *this = std::move(other); } + IntegerListNode& operator=(IntegerListNode&& other) { + value_ = other.value_; + node = std::move(other.node); + return *this; + } + + int value() const { return value_; } + firebase::intrusive_list_node node; // NOLINT + + private: + int value_; + + // Disallow copying. + IntegerListNode(const IntegerListNode&); + IntegerListNode& operator=(const IntegerListNode&); +}; + +bool IntegerListNodeComparitor(const IntegerListNode& a, + const IntegerListNode& b) { + return a.value() < b.value(); +} + +bool operator<(const IntegerListNode& a, const IntegerListNode& b) { + return a.value() < b.value(); +} + +bool operator==(const IntegerListNode& a, const IntegerListNode& b) { + return a.value() == b.value(); +} + +class intrusive_list_test : public testing::Test { + protected: + intrusive_list_test() + : list_(&IntegerListNode::node), + one_(1), + two_(2), + three_(3), + four_(4), + five_(5), + six_(6), + seven_(7), + eight_(8), + nine_(9), + ten_(10), + twenty_(20), + thirty_(30), + fourty_(40), + fifty_(50) {} + + firebase::intrusive_list list_; + IntegerListNode one_; + IntegerListNode two_; + IntegerListNode three_; + IntegerListNode four_; + IntegerListNode five_; + IntegerListNode six_; + IntegerListNode seven_; + IntegerListNode eight_; + IntegerListNode nine_; + IntegerListNode ten_; + IntegerListNode twenty_; + IntegerListNode thirty_; + IntegerListNode fourty_; + IntegerListNode fifty_; +}; + +TEST_F(intrusive_list_test, push_back) { + EXPECT_TRUE(!one_.node.in_list()); + EXPECT_TRUE(!two_.node.in_list()); + EXPECT_TRUE(!three_.node.in_list()); + EXPECT_TRUE(!four_.node.in_list()); + EXPECT_TRUE(!five_.node.in_list()); + + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_EQ(1, list_.front().value()); + EXPECT_EQ(5, list_.back().value()); +} + +#ifndef NO_DEATH_TESTS +TEST_F(intrusive_list_test, push_back_failure) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + EXPECT_DEATH(list_.push_back(five_), "."); +} +#endif // NO_DEATH_TESTS + +TEST_F(intrusive_list_test, pop_back) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + EXPECT_EQ(5, list_.back().value()); + list_.pop_back(); + EXPECT_EQ(4, list_.back().value()); + list_.pop_back(); + EXPECT_EQ(3, list_.back().value()); + list_.pop_back(); + list_.push_back(four_); + EXPECT_EQ(4, list_.back().value()); +} + +TEST_F(intrusive_list_test, push_front) { + list_.push_front(one_); + list_.push_front(two_); + list_.push_front(three_); + list_.push_front(four_); + list_.push_front(five_); + + auto iter = list_.begin(); + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_EQ(5, list_.front().value()); + EXPECT_EQ(1, list_.back().value()); +} + +#ifndef NO_DEATH_TESTS +TEST_F(intrusive_list_test, push_front_failure) { + list_.push_front(five_); + list_.push_front(four_); + list_.push_front(three_); + list_.push_front(two_); + list_.push_front(one_); + EXPECT_DEATH(list_.push_front(one_), "."); +} +#endif // NO_DEATH_TESTS + +TEST_F(intrusive_list_test, destructor) { + list_.push_back(one_); + list_.push_back(two_); + { + // These should remove themselves when they go out of scope. + IntegerListNode one_hundred(100); + IntegerListNode two_hundred(200); + list_.push_back(one_hundred); + list_.push_back(two_hundred); + } + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_EQ(1, list_.front().value()); + EXPECT_EQ(5, list_.back().value()); +} + +TEST_F(intrusive_list_test, move_node) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + // Generally, when moving something it would be done implicitly when the + // object holding it moves. This is just to demonstrate that it moves the + // pointers around correctly when it does move. + // + // two_.node has four_.node's location in the list moved into it. four_.node + // is left in a valid but unspecified state. + two_.node = std::move(four_.node); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, rbegin_rend) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.rbegin(); + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(list_.rend(), iter); +} + +TEST_F(intrusive_list_test, crbegin_crend) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.crbegin(); + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(list_.crend(), iter); +} + +TEST_F(intrusive_list_test, clear) { + EXPECT_TRUE(list_.empty()); + + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + EXPECT_FALSE(list_.empty()); + + list_.clear(); + EXPECT_TRUE(list_.empty()); +} + +TEST_F(intrusive_list_test, insert) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + ++iter; + list_.insert(iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_before) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + ++iter; + firebase::intrusive_list:: + insert_before<&IntegerListNode::node>(*iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_after) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + firebase::intrusive_list:: + insert_after<&IntegerListNode::node>(*iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_begin) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + list_.insert(iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_end) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + auto iter = list_.begin(); + ++iter; + ++iter; + ++iter; + ++iter; + ++iter; + list_.insert(iter, ten_); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, insert_iter) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + std::vector list_nodes; + list_nodes.push_back(IntegerListNode(100)); + list_nodes.push_back(IntegerListNode(200)); + list_nodes.push_back(IntegerListNode(300)); + + auto iter = list_.begin(); + ++iter; + ++iter; + list_.insert(iter, list_nodes.begin(), list_nodes.end()); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(100, iter->value()); + ++iter; + EXPECT_EQ(200, iter->value()); + ++iter; + EXPECT_EQ(300, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, size) { + EXPECT_EQ(0u, list_.size()); + EXPECT_TRUE(list_.empty()); + list_.push_back(one_); + EXPECT_EQ(1u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_back(two_); + EXPECT_EQ(2u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_front(three_); + EXPECT_EQ(3u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_back(four_); + EXPECT_EQ(4u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.push_front(five_); + EXPECT_EQ(5u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_front(); + EXPECT_EQ(4u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_back(); + EXPECT_EQ(3u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_front(); + EXPECT_EQ(2u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_back(); + EXPECT_EQ(1u, list_.size()); + EXPECT_FALSE(list_.empty()); + list_.pop_front(); + EXPECT_EQ(0u, list_.size()); + EXPECT_TRUE(list_.empty()); +} + +TEST_F(intrusive_list_test, unique) { + IntegerListNode another_one(1); + IntegerListNode another_three(3); + IntegerListNode another_five(5); + IntegerListNode another_five_again(5); + + list_.push_back(one_); + list_.push_back(another_one); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(another_three); + list_.push_back(four_); + list_.push_back(five_); + list_.push_back(another_five); + list_.push_back(another_five_again); + + list_.unique(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + EXPECT_TRUE(!another_one.node.in_list()); + EXPECT_TRUE(!another_three.node.in_list()); + EXPECT_TRUE(!another_five.node.in_list()); + EXPECT_TRUE(!another_five_again.node.in_list()); +} + +TEST_F(intrusive_list_test, unique_predicate) { + IntegerListNode another_one(1); + IntegerListNode another_three(3); + IntegerListNode another_five(5); + IntegerListNode another_five_again(5); + + list_.push_back(one_); + list_.push_back(another_one); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(another_three); + list_.push_back(four_); + list_.push_back(five_); + list_.push_back(another_five); + list_.push_back(another_five_again); + + list_.unique(std::equal_to()); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + EXPECT_TRUE(!another_one.node.in_list()); + EXPECT_TRUE(!another_three.node.in_list()); + EXPECT_TRUE(!another_five.node.in_list()); + EXPECT_TRUE(!another_five_again.node.in_list()); +} + +TEST_F(intrusive_list_test, sort_in_order) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, sort_reverse_order) { + list_.push_back(five_); + list_.push_back(four_); + list_.push_back(three_); + list_.push_back(two_); + list_.push_back(one_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, sort_random_order) { + list_.push_back(two_); + list_.push_back(four_); + list_.push_back(five_); + list_.push_back(one_); + list_.push_back(three_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, sort_short_list) { + list_.push_back(two_); + list_.push_back(one_); + + list_.sort(); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, splice_empty) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + + list_.splice(list_.begin(), other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, splice_other_at_beginning) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(ten_); + other_list.push_back(twenty_); + other_list.push_back(thirty_); + other_list.push_back(fourty_); + other_list.push_back(fifty_); + + list_.splice(list_.begin(), other_list); + + auto iter = list_.begin(); + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, splice_other_at_end) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(ten_); + other_list.push_back(twenty_); + other_list.push_back(thirty_); + other_list.push_back(fourty_); + other_list.push_back(fifty_); + + list_.splice(list_.end(), other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, splice_other_at_middle) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(ten_); + other_list.push_back(twenty_); + other_list.push_back(thirty_); + other_list.push_back(fourty_); + other_list.push_back(fifty_); + + auto iter = list_.begin(); + ++iter; + ++iter; + ++iter; + list_.splice(iter, other_list); + + iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_alternating) { + list_.push_back(one_); + list_.push_back(three_); + list_.push_back(five_); + list_.push_back(seven_); + list_.push_back(nine_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(two_); + other_list.push_back(four_); + other_list.push_back(six_); + other_list.push_back(eight_); + other_list.push_back(ten_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_alternating2) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(five_); + list_.push_back(six_); + list_.push_back(nine_); + list_.push_back(ten_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(three_); + other_list.push_back(four_); + other_list.push_back(seven_); + other_list.push_back(eight_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_this_other) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(six_); + other_list.push_back(seven_); + other_list.push_back(eight_); + other_list.push_back(nine_); + other_list.push_back(ten_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +TEST_F(intrusive_list_test, merge_other_this) { + list_.push_back(six_); + list_.push_back(seven_); + list_.push_back(eight_); + list_.push_back(nine_); + list_.push_back(ten_); + + firebase::intrusive_list other_list(&IntegerListNode::node); + other_list.push_back(one_); + other_list.push_back(two_); + other_list.push_back(three_); + other_list.push_back(four_); + other_list.push_back(five_); + + list_.merge(other_list); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(6, iter->value()); + ++iter; + EXPECT_EQ(7, iter->value()); + ++iter; + EXPECT_EQ(8, iter->value()); + ++iter; + EXPECT_EQ(9, iter->value()); + ++iter; + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other_list.begin(); + EXPECT_EQ(other_list.end(), iter); +} + +#if defined(FIREBASE_USE_MOVE_OPERATORS) +TEST_F(intrusive_list_test, move_constructor) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other(std::move(list_)); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + + auto iter = other.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(other.end(), iter); +} + +TEST_F(intrusive_list_test, move_assignment) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other(&IntegerListNode::node); + other = std::move(list_); + + EXPECT_TRUE(one_.node.in_list()); + EXPECT_TRUE(two_.node.in_list()); + EXPECT_TRUE(three_.node.in_list()); + EXPECT_TRUE(four_.node.in_list()); + EXPECT_TRUE(five_.node.in_list()); + + auto iter = other.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(other.end(), iter); +} +#endif + +TEST_F(intrusive_list_test, swap) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + firebase::intrusive_list other(&IntegerListNode::node); + other.push_back(ten_); + other.push_back(twenty_); + other.push_back(thirty_); + other.push_back(fourty_); + other.push_back(fifty_); + + list_.swap(other); + + auto iter = list_.begin(); + EXPECT_EQ(10, iter->value()); + ++iter; + EXPECT_EQ(20, iter->value()); + ++iter; + EXPECT_EQ(30, iter->value()); + ++iter; + EXPECT_EQ(40, iter->value()); + ++iter; + EXPECT_EQ(50, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); + + iter = other.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(other.end(), iter); +} + +TEST_F(intrusive_list_test, swap_self) { + list_.push_back(one_); + list_.push_back(two_); + list_.push_back(three_); + list_.push_back(four_); + list_.push_back(five_); + + list_.swap(list_); + + auto iter = list_.begin(); + EXPECT_EQ(1, iter->value()); + ++iter; + EXPECT_EQ(2, iter->value()); + ++iter; + EXPECT_EQ(3, iter->value()); + ++iter; + EXPECT_EQ(4, iter->value()); + ++iter; + EXPECT_EQ(5, iter->value()); + ++iter; + EXPECT_EQ(list_.end(), iter); +} + +TEST_F(intrusive_list_test, erase_iterator) { + IntegerListNode *e[10]; + + // Create a list with 10 items. + for (int i = 0; i < 10; ++i) { + e[i] = new IntegerListNode(i); + list_.push_back(*e[i]); + } + + // Test that erase(iterator) works. + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(list_.size(), 10 - i); + + using iterator = firebase::intrusive_list::iterator; + iterator it = std::find(list_.begin(), list_.end(), IntegerListNode(i)); + iterator next_it = list_.erase(it); + if (i == 9) { + EXPECT_EQ(next_it, list_.end()); + } else { + EXPECT_NE(next_it->value(), i); + } + + EXPECT_EQ(list_.size(), 10 - i - 1); + delete e[i]; + } +} + +TEST_F(intrusive_list_test, erase_range) { + IntegerListNode *e[10]; + + // Create a list with 10 items. + for (int i = 0; i < 10; ++i) { + e[i] = new IntegerListNode(i); + list_.push_back(*e[i]); + } + + using iterator = firebase::intrusive_list::iterator; + + // Test that erase(iterator, iterator) with a null range has no effect. + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(list_.size(), 10); + iterator range_begin = std::find(list_.begin(), list_.end(), + IntegerListNode(i)); + iterator range_end = range_begin; + iterator result = list_.erase(range_begin, range_end); + EXPECT_EQ(list_.size(), 10); + EXPECT_EQ(result, range_end); + EXPECT_NE(result, list_.end()); + EXPECT_EQ(result->value(), i); + } + + // Test that erase(iterator, iterator) with a non-empty range works. + for (int i = 0; i < 10; i += 2) { + EXPECT_EQ(list_.size(), 10 - i); + iterator range_begin = std::find(list_.begin(), list_.end(), + IntegerListNode(i)); + iterator range_end = std::find(list_.begin(), list_.end(), + IntegerListNode(i + 2)); + iterator result = list_.erase(range_begin, range_end); + EXPECT_EQ(result, range_end); + if (i + 2 == 10) { + EXPECT_EQ(result, list_.end()); + } else { + EXPECT_EQ(result->value(), i + 2); + } + EXPECT_EQ(list_.size(), 10 - i - 2); + delete e[i]; + delete e[i + 1]; + } + EXPECT_EQ(list_.size(), 0); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/app/tests/jobject_reference_test.cc b/app/tests/jobject_reference_test.cc new file mode 100644 index 0000000000..06fe955085 --- /dev/null +++ b/app/tests/jobject_reference_test.cc @@ -0,0 +1,162 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/jobject_reference.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +// For firebase::util::JStringToString. +#include "app/src/util_android.h" +#include "testing/run_all_tests.h" + +using firebase::internal::JObjectReference; +using firebase::util::JStringToString; +using testing::Eq; +using testing::IsNull; +using testing::NotNull; + +JOBJECT_REFERENCE(JObjectReferenceAlias); + +// Tests for JObjectReference. +class JObjectReferenceTest : public ::testing::Test { + protected: + void SetUp() override { + env_ = firebase::testing::cppsdk::GetTestJniEnv(); + ASSERT_TRUE(env_ != nullptr); + } + + JNIEnv *env_; + + static const char *const kTestString; +}; + +const char *const JObjectReferenceTest::kTestString = "Testing testing 1 2 3"; + +TEST_F(JObjectReferenceTest, ConstructEmpty) { + JObjectReference ref(env_); + JObjectReferenceAlias alias(env_); + EXPECT_THAT(ref.GetJNIEnv(), Eq(env_)); + EXPECT_THAT(ref.java_vm(), NotNull()); + EXPECT_THAT(ref.object(), IsNull()); + EXPECT_THAT(*ref, IsNull()); + EXPECT_THAT(alias.GetJNIEnv(), Eq(env_)); + EXPECT_THAT(alias.java_vm(), NotNull()); + EXPECT_THAT(alias.object(), IsNull()); + EXPECT_THAT(*alias, IsNull()); +} + +TEST_F(JObjectReferenceTest, ConstructDestruct) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref(env_, java_string); + JObjectReferenceAlias alias(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_THAT(JStringToString(env_, ref.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, *ref), Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, *alias), Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, CopyConstruct) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref1(env_, java_string); + env_->DeleteLocalRef(java_string); + JObjectReference ref2(ref1); + JObjectReferenceAlias alias1(ref1); + JObjectReferenceAlias alias2(alias1); + EXPECT_THAT(JStringToString(env_, ref1.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, ref2.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias1.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias2.object()), + Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, Move) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref1(env_, java_string); + env_->DeleteLocalRef(java_string); + JObjectReference ref2 = std::move(ref1); + EXPECT_THAT(JStringToString(env_, ref2.object()), + Eq(std::string(kTestString))); + JObjectReferenceAlias alias1(std::move(ref2)); + EXPECT_THAT(JStringToString(env_, alias1.object()), + Eq(std::string(kTestString))); + JObjectReferenceAlias alias2(env_); + alias2 = std::move(alias1); + EXPECT_THAT(JStringToString(env_, alias2.object()), + Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, Copy) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref1(env_, java_string); + env_->DeleteLocalRef(java_string); + JObjectReference ref2(env_); + ref2 = ref1; + JObjectReferenceAlias alias(env_); + alias = ref2; + EXPECT_THAT(JStringToString(env_, ref1.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, ref2.object()), + Eq(std::string(kTestString))); + EXPECT_THAT(JStringToString(env_, alias.object()), + Eq(std::string(kTestString))); +} + +TEST_F(JObjectReferenceTest, Set) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_THAT(JStringToString(env_, ref.object()), + Eq(std::string(kTestString))); + ref.Set(nullptr); + EXPECT_THAT(ref.object(), IsNull()); +} + +TEST_F(JObjectReferenceTest, GetLocalRef) { + jobject java_string = env_->NewStringUTF(kTestString); + JObjectReference ref(env_, java_string); + jobject local = ref.GetLocalRef(); + EXPECT_THAT(JStringToString(env_, local), Eq(std::string(kTestString))); + env_->DeleteLocalRef(local); + + JObjectReferenceAlias alias(env_, java_string); + local = alias.GetLocalRef(); + EXPECT_THAT(JStringToString(env_, local), Eq(std::string(kTestString))); + env_->DeleteLocalRef(local); +} + +TEST_F(JObjectReferenceTest, FromGlobalReference) { + jobject java_string = env_->NewStringUTF(kTestString); + jobject java_string_alias = env_->NewLocalRef(java_string); + JObjectReference ref = + JObjectReference::FromLocalReference(env_, java_string); + JObjectReferenceAlias alias( + JObjectReferenceAlias::FromLocalReference(env_, java_string_alias)); + EXPECT_NE(nullptr, ref.object()); + EXPECT_NE(nullptr, alias.object()); + + JObjectReference nullref = + JObjectReference::FromLocalReference(env_, nullptr); + JObjectReferenceAlias alias_null( + JObjectReferenceAlias::FromLocalReference(env_, nullptr)); + EXPECT_EQ(nullptr, nullref.object()); + EXPECT_EQ(nullptr, alias_null.object()); +} diff --git a/app/tests/locale_test.cc b/app/tests/locale_test.cc new file mode 100644 index 0000000000..478786894c --- /dev/null +++ b/app/tests/locale_test.cc @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/locale.h" + +#include + +#include "app/src/include/firebase/internal/platform.h" +#include "app/src/log.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#if FIREBASE_PLATFORM_WINDOWS + +#else +#include +#endif // FIREBASE_PLATFORM_WINDOWS + +namespace firebase { +namespace internal { + +class LocaleTest : public ::testing::Test {}; + +TEST_F(LocaleTest, TestGetTimezone) { + std::string tz = GetTimezone(); + LogInfo("GetTimezone() returned '%s'", tz.c_str()); + // There is not a set format for timezones, so we must assume success if it + // was non-empty. + EXPECT_NE(tz, ""); +} + +TEST_F(LocaleTest, TestGetLocale) { + std::string loc = GetLocale(); + LogInfo("GetLocale() returned '%s'", loc.c_str()); + EXPECT_NE(loc, ""); + // Make sure this looks like a locale, e.g. has at least 5 characters and + // contains an underscore. + EXPECT_GE(loc.size(), 5); + EXPECT_NE(loc.find("_"), std::string::npos); +} + +} // namespace internal +} // namespace firebase diff --git a/app/tests/log_test.cc b/app/tests/log_test.cc new file mode 100644 index 0000000000..30b1b4783e --- /dev/null +++ b/app/tests/log_test.cc @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/log.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +// The test-cases here are by no means exhaustive. We only make sure the log +// code does not break. Whether logs are output is highly device-dependent and +// testing that is not right now the main goal here. + +TEST(LogTest, TestSetAndGetLogLevel) { + // Try to set log-level and verify we get what we set. + SetLogLevel(kLogLevelDebug); + EXPECT_EQ(kLogLevelDebug, GetLogLevel()); + + SetLogLevel(kLogLevelError); + EXPECT_EQ(kLogLevelError, GetLogLevel()); +} + +TEST(LogDeathTest, TestLogAssert) { + // Try to make assertion and verify it dies. + SetLogLevel(kLogLevelVerbose); +// Somehow the death test does not work on ios emulator. +#if !defined(__APPLE__) + EXPECT_DEATH(LogAssert("should die"), ""); +#endif // !defined(__APPLE__) +} + +TEST(LogTest, TestLogLevelBelowAssert) { + // Try other non-aborting log levels. + SetLogLevel(kLogLevelVerbose); + // TODO(zxu): Try to catch the logs using log callback in order to verify the + // log message. + LogDebug("debug message"); + LogInfo("info message"); + LogWarning("warning message"); + LogError("error message"); +} + +} // namespace firebase diff --git a/app/tests/logger_test.cc b/app/tests/logger_test.cc new file mode 100644 index 0000000000..d082afed3f --- /dev/null +++ b/app/tests/logger_test.cc @@ -0,0 +1,283 @@ +// Copyright 2019 Google LLC +// +// 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 "app/src/logger.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace internal { +namespace { + +static const size_t kBufferSize = 100; + +class FakeLogger : public LoggerBase { + public: + FakeLogger() + : logged_message_(), + logged_message_level_(static_cast(-1)), + log_level_(kLogLevelInfo) {} + + void SetLogLevel(LogLevel log_level) override { log_level_ = log_level; } + LogLevel GetLogLevel() const override { return log_level_; } + + const std::string& logged_message() const { return logged_message_; } + LogLevel logged_message_level() const { return logged_message_level_; } + + private: + void LogMessageImplV(LogLevel log_level, const char* format, + va_list args) const override { + logged_message_level_ = log_level; + char buffer[kBufferSize]; + vsnprintf(buffer, kBufferSize, format, args); + logged_message_ = buffer; + } + + mutable std::string logged_message_; + mutable LogLevel logged_message_level_; + + mutable LogLevel log_level_; +}; + +TEST(LoggerTest, GetSetLogLevel) { + Logger logger(nullptr); + EXPECT_EQ(logger.GetLogLevel(), kLogLevelInfo); + logger.SetLogLevel(kLogLevelVerbose); + EXPECT_EQ(logger.GetLogLevel(), kLogLevelVerbose); + + Logger logger2(nullptr, kLogLevelDebug); + EXPECT_EQ(logger2.GetLogLevel(), kLogLevelDebug); + logger2.SetLogLevel(kLogLevelInfo); + EXPECT_EQ(logger2.GetLogLevel(), kLogLevelInfo); +} + +TEST(LoggerTest, LogWithEachFunction) { + FakeLogger logger; + + // Ensure everything gets through. + logger.SetLogLevel(kLogLevelVerbose); + + logger.LogDebug("LogDebug %i", 1); + EXPECT_EQ(logger.logged_message_level(), kLogLevelDebug); + EXPECT_EQ(logger.logged_message(), "LogDebug 1"); + + logger.LogInfo("LogInfo %i", 2); + EXPECT_EQ(logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(logger.logged_message(), "LogInfo 2"); + + logger.LogWarning("LogWarning %i", 3); + EXPECT_EQ(logger.logged_message_level(), kLogLevelWarning); + EXPECT_EQ(logger.logged_message(), "LogWarning 3"); + + logger.LogError("LogError %i", 4); + EXPECT_EQ(logger.logged_message_level(), kLogLevelError); + EXPECT_EQ(logger.logged_message(), "LogError 4"); + + logger.LogAssert("LogAssert %i", 5); + EXPECT_EQ(logger.logged_message_level(), kLogLevelAssert); + EXPECT_EQ(logger.logged_message(), "LogAssert 5"); + + logger.LogMessage(kLogLevelInfo, "LogMessage %i", 6); + EXPECT_EQ(logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(logger.logged_message(), "LogMessage 6"); +} + +TEST(LoggerTest, FilteringPermissive) { + FakeLogger logger; + + logger.SetLogLevel(kLogLevelVerbose); + + logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(logger.logged_message(), "Verbose log"); + + logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(logger.logged_message(), "Debug log"); + + logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(logger.logged_message(), "Info log"); + + logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(logger.logged_message(), "Warning log"); + + logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(logger.logged_message(), "Error log"); + + logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, FilteringMiddling) { + FakeLogger logger; + + logger.SetLogLevel(kLogLevelWarning); + + logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(logger.logged_message(), "Warning log"); + + logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(logger.logged_message(), "Error log"); + + logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, FilteringStrict) { + FakeLogger logger; + + logger.SetLogLevel(kLogLevelAssert); + + logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(logger.logged_message(), ""); + + logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, ChainedLogWithEachFunction) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelVerbose); + child_logger.SetLogLevel(kLogLevelVerbose); + + child_logger.LogDebug("LogDebug %i", 1); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelDebug); + EXPECT_EQ(parent_logger.logged_message(), "LogDebug 1"); + + child_logger.LogInfo("LogInfo %i", 2); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(parent_logger.logged_message(), "LogInfo 2"); + + child_logger.LogWarning("LogWarning %i", 3); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelWarning); + EXPECT_EQ(parent_logger.logged_message(), "LogWarning 3"); + + child_logger.LogError("LogError %i", 4); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelError); + EXPECT_EQ(parent_logger.logged_message(), "LogError 4"); + + child_logger.LogAssert("LogAssert %i", 5); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelAssert); + EXPECT_EQ(parent_logger.logged_message(), "LogAssert 5"); + + child_logger.LogMessage(kLogLevelInfo, "LogMessage %i", 6); + EXPECT_EQ(parent_logger.logged_message_level(), kLogLevelInfo); + EXPECT_EQ(parent_logger.logged_message(), "LogMessage 6"); +} + +TEST(LoggerTest, ChainedFilteringSameLevel) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelInfo); + child_logger.SetLogLevel(kLogLevelInfo); + + child_logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(parent_logger.logged_message(), "Info log"); + + child_logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(parent_logger.logged_message(), "Warning log"); + + child_logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(parent_logger.logged_message(), "Error log"); + + child_logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(parent_logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, ChainedFilteringStricterChildLogger) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelInfo); + child_logger.SetLogLevel(kLogLevelError); + + child_logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(parent_logger.logged_message(), "Error log"); + + child_logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(parent_logger.logged_message(), "Assert log"); +} + +TEST(LoggerTest, ChainedFilteringMorePermissiveChildLogger) { + FakeLogger parent_logger; + Logger child_logger(&parent_logger); + + parent_logger.SetLogLevel(kLogLevelError); + child_logger.SetLogLevel(kLogLevelInfo); + + child_logger.LogMessage(kLogLevelVerbose, "Verbose log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelDebug, "Debug log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelInfo, "Info log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelWarning, "Warning log"); + EXPECT_EQ(parent_logger.logged_message(), ""); + + child_logger.LogMessage(kLogLevelError, "Error log"); + EXPECT_EQ(parent_logger.logged_message(), "Error log"); + + child_logger.LogMessage(kLogLevelAssert, "Assert log"); + EXPECT_EQ(parent_logger.logged_message(), "Assert log"); +} + +} // namespace +} // namespace internal +} // namespace firebase diff --git a/app/tests/optional_test.cc b/app/tests/optional_test.cc new file mode 100644 index 0000000000..396ea86799 --- /dev/null +++ b/app/tests/optional_test.cc @@ -0,0 +1,446 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/optional.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// Using Mocks to count the number of times a constructor, destructor, +// or (move|copy) (constructor|assignment operator) is called is not easy to do +// directly because gMock requires marking the methods to be called virtual. +// Instead, we create a special wrapper Mock object that calls these virtual +// functions instead, so that they can be counted. +class SpecialFunctionsNotifier { + public: + virtual ~SpecialFunctionsNotifier() {} + + virtual void Construct() = 0; + virtual void Copy() = 0; +#ifdef FIREBASE_USE_MOVE_OPERATORS + virtual void Move() = 0; +#endif + virtual void Destruct() = 0; +}; + +// This class exists only to call through to the underlying +// SpecialFunctionNotifier so that those calls can be counted. +class SpecialFunctionsNotifierWrapper { + public: + SpecialFunctionsNotifierWrapper() { s_notifier_->Construct(); } + SpecialFunctionsNotifierWrapper( + const SpecialFunctionsNotifierWrapper& other) { + s_notifier_->Copy(); + } + SpecialFunctionsNotifierWrapper& operator=( + const SpecialFunctionsNotifierWrapper& other) { + s_notifier_->Copy(); + return *this; + } + +#ifdef FIREBASE_USE_MOVE_OPERATORS + SpecialFunctionsNotifierWrapper(SpecialFunctionsNotifierWrapper&& other) { + s_notifier_->Move(); + } + SpecialFunctionsNotifierWrapper& operator=( + SpecialFunctionsNotifierWrapper&& other) { + s_notifier_->Move(); + return *this; + } +#endif + + ~SpecialFunctionsNotifierWrapper() { s_notifier_->Destruct(); } + + static SpecialFunctionsNotifier* s_notifier_; +}; + +SpecialFunctionsNotifier* SpecialFunctionsNotifierWrapper::s_notifier_ = + nullptr; + +class SpecialFunctionsNotifierMock : public SpecialFunctionsNotifier { + public: + MOCK_METHOD(void, Construct, (), (override)); + MOCK_METHOD(void, Copy, (), (override)); +#ifdef FIREBASE_USE_MOVE_OPERATORS + MOCK_METHOD(void, Move, (), (override)); +#endif + MOCK_METHOD(void, Destruct, (), (override)); +}; + +// A simple class with a method on it, used for testing the arrow operator of +// Optional. +class IntHolder { + public: + explicit IntHolder(int value) : value_(value) {} + int GetValue() const { return value_; } + + private: + int value_; +}; + +// Helper class used to setup mock expect calls due to the complexities of move +// enabled or not +class ExpectCallSetup { + public: + explicit ExpectCallSetup(SpecialFunctionsNotifierMock* mock_notifier) + : mock_notifier_(mock_notifier) {} + + ExpectCallSetup& Construct(size_t expectecCallCount) { + EXPECT_CALL(*mock_notifier_, Construct()).Times(expectecCallCount); + return *this; + } + + ExpectCallSetup& CopyAndMove(size_t expectecCopyCallCount, + size_t expectecMoveCallCount) { +#ifdef FIREBASE_USE_MOVE_OPERATORS + EXPECT_CALL(*mock_notifier_, Copy()).Times(expectecCopyCallCount); + EXPECT_CALL(*mock_notifier_, Move()).Times(expectecMoveCallCount); +#else + EXPECT_CALL(*mock_notifier_, Copy()). + Times(expectecCopyCallCount + expectecMoveCallCount); +#endif + return *this; + } + + ExpectCallSetup& Destruct(size_t expectecCallCount) { + EXPECT_CALL(*mock_notifier_, Destruct()).Times(expectecCallCount); + return *this; + } + + SpecialFunctionsNotifierMock* mock_notifier_; +}; + +class OptionalTest : public ::testing::Test, protected ExpectCallSetup { + protected: + OptionalTest() + : ExpectCallSetup(&mock_notifier_) + {} + + void SetUp() override { + SpecialFunctionsNotifierWrapper::s_notifier_ = &mock_notifier_; + } + + void TearDown() override { + SpecialFunctionsNotifierWrapper::s_notifier_ = nullptr; + } + + ExpectCallSetup& SetupExpectCall() { + return *this; + } + + SpecialFunctionsNotifierMock mock_notifier_; +}; + +TEST_F(OptionalTest, DefaultConstructor) { + Optional optional_int; + EXPECT_FALSE(optional_int.has_value()); + + Optional optional_struct; + EXPECT_FALSE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, CopyConstructor) { + Optional optional_int(9999); + + Optional copy_of_optional_int(optional_int); + EXPECT_TRUE(copy_of_optional_int.has_value()); + EXPECT_EQ(copy_of_optional_int.value(), 9999); + + Optional another_copy_of_optional_int = optional_int; + EXPECT_TRUE(another_copy_of_optional_int.has_value()); + EXPECT_EQ(another_copy_of_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(2, 1) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + + Optional copy_of_optional_struct( + optional_struct); + EXPECT_TRUE(copy_of_optional_struct.has_value()); + + Optional another_copy_of_optional_struct = + optional_struct; + EXPECT_TRUE(another_copy_of_optional_struct.has_value()); +} + +TEST_F(OptionalTest, CopyAssignment) { + Optional optional_int(9999); + Optional another_optional_int(42); + another_optional_int = optional_int; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + EXPECT_TRUE(another_optional_int.has_value()); + EXPECT_EQ(another_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(2) + .CopyAndMove(1, 2) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + Optional another_optional_struct( + SpecialFunctionsNotifierWrapper{}); + another_optional_struct = optional_struct; + EXPECT_TRUE(optional_struct.has_value()); + EXPECT_TRUE(another_optional_struct.has_value()); +} + +TEST_F(OptionalTest, CopyAssignmentSelf) { + Optional optional_int(9999); + optional_int = *&optional_int; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(1, 1) + .Destruct(2); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + optional_struct = *&optional_struct; + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, MoveConstructor) { + Optional optional_int(9999); + + Optional moved_optional_int(std::move(optional_int)); + EXPECT_TRUE(moved_optional_int.has_value()); + EXPECT_EQ(moved_optional_int.value(), 9999); + + Optional another_moved_optional_int = std::move(moved_optional_int); + EXPECT_TRUE(another_moved_optional_int.has_value()); + EXPECT_EQ(another_moved_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(0, 3) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + + Optional copy_of_optional_struct( + std::move(optional_struct)); + EXPECT_TRUE(copy_of_optional_struct.has_value()); + + Optional another_copy_of_optional_struct = + std::move(copy_of_optional_struct); + EXPECT_TRUE(another_copy_of_optional_struct.has_value()); +} + +TEST_F(OptionalTest, MoveAssignment) { + Optional optional_int(9999); + Optional another_optional_int(42); + another_optional_int = std::move(optional_int); + + EXPECT_TRUE(another_optional_int.has_value()); + EXPECT_EQ(another_optional_int.value(), 9999); + + SetupExpectCall() + .Construct(2) + .CopyAndMove(0, 3) + .Destruct(4); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + Optional another_optional_struct( + SpecialFunctionsNotifierWrapper{}); + another_optional_struct = std::move(optional_struct); + + EXPECT_TRUE(another_optional_struct.has_value()); +} + +TEST_F(OptionalTest, Destructor) { + SetupExpectCall() + .Construct(2) + .CopyAndMove(0, 2) + .Destruct(4); + + // Verify the destructor is called when object goes out of scope. + { + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + (void)optional_struct; + } + // Verify the destructor is called when reset is called. + { + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + optional_struct.reset(); + } +} + +TEST_F(OptionalTest, ValueConstructor) { + Optional optional_int(1337); + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 1337); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(1, 0) + .Destruct(2); + + SpecialFunctionsNotifierWrapper value{}; + Optional optional_struct(value); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueMoveConstructor) { + SetupExpectCall() + .Construct(1) + .CopyAndMove(0, 1) + .Destruct(2); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueCopyAssignmentToUnpopulatedOptional) { + Optional optional_int; + optional_int = 9999; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + + SetupExpectCall() + .Construct(1) + .CopyAndMove(1, 0) + .Destruct(2); + + Optional optional_struct; + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = my_struct; + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueCopyAssignmentToPopulatedOptional) { + Optional optional_int(27); + optional_int = 9999; + EXPECT_TRUE(optional_int.has_value()); + EXPECT_EQ(optional_int.value(), 9999); + + SetupExpectCall() + .Construct(2) + .CopyAndMove(1, 1) + .Destruct(3); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = my_struct; + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueMoveAssignmentToUnpopulatedOptional) { + SetupExpectCall() + .Construct(1) + .CopyAndMove(0, 1) + .Destruct(2); + + Optional optional_struct; + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = std::move(my_struct); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ValueMoveAssignmentToPopulatedOptional) { + SetupExpectCall() + .Construct(2) + .CopyAndMove(0, 2) + .Destruct(3); + + Optional optional_struct( + SpecialFunctionsNotifierWrapper{}); + SpecialFunctionsNotifierWrapper my_struct{}; + optional_struct = std::move(my_struct); + EXPECT_TRUE(optional_struct.has_value()); +} + +TEST_F(OptionalTest, ArrowOperator) { + Optional optional_int_holder(IntHolder(12345)); + EXPECT_EQ(optional_int_holder->GetValue(), 12345); +} + +TEST_F(OptionalTest, HasValue) { + Optional optional_int; + EXPECT_FALSE(optional_int.has_value()); + + optional_int = 12345; + EXPECT_TRUE(optional_int.has_value()); + + optional_int.reset(); + EXPECT_FALSE(optional_int.has_value()); +} + +TEST_F(OptionalTest, ValueDeathTest) { + Optional empty; + EXPECT_DEATH(empty.value(), ""); +} + +TEST_F(OptionalTest, ValueOr) { + Optional optional_int; + EXPECT_EQ(optional_int.value_or(67890), 67890); + + optional_int = 12345; + EXPECT_EQ(optional_int.value_or(67890), 12345); +} + +TEST_F(OptionalTest, EqualityOperator) { + Optional lhs(123456); + Optional rhs(123456); + Optional wrong(654321); + Optional empty; + Optional another_empty; + + EXPECT_TRUE(lhs == rhs); + EXPECT_FALSE(lhs != rhs); + EXPECT_FALSE(lhs == wrong); + EXPECT_TRUE(lhs != wrong); + + EXPECT_FALSE(empty == rhs); + EXPECT_TRUE(empty != rhs); + EXPECT_TRUE(empty == another_empty); + EXPECT_FALSE(empty != another_empty); +} + +TEST_F(OptionalTest, OptionalFromPointer) { + int value = 100; + int* value_ptr = &value; + int* value_nullptr = nullptr; + Optional optional_with_value = OptionalFromPointer(value_ptr); + Optional optional_without_value = OptionalFromPointer(value_nullptr); + + EXPECT_TRUE(optional_with_value.has_value()); + EXPECT_EQ(optional_with_value.value(), 100); + EXPECT_FALSE(optional_without_value.has_value()); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/app/tests/path_test.cc b/app/tests/path_test.cc new file mode 100644 index 0000000000..bdce82eda4 --- /dev/null +++ b/app/tests/path_test.cc @@ -0,0 +1,452 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/path.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +using ::firebase::Optional; +using ::firebase::Path; +using ::testing::Eq; +using ::testing::StrEq; + +TEST(PathTests, DefaultConstructor) { + Path path; + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_TRUE(path.empty()); +} + +TEST(PathTests, StringConstructor) { + Path path; + + // Empty string + path = Path(""); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); + + // Root folder + path = Path("/"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); + + // Root Folder with plenty slashes + path = Path("//////"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); + + // Correctly formatted string. + path = Path("test/foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Leading slash. + path = Path("/test/foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Trailing slash. + path = Path("test/foo/bar/"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Leading and trailing slash. + path = Path("/test/foo/bar/"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Internal slashes. + path = Path("/test/////foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Slashes everywhere! + path = Path("///test/////foo//bar///"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Backslashes. + path = Path("///test\\foo\\bar///"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test\\foo\\bar")); + EXPECT_THAT(path.str(), StrEq("test\\foo\\bar")); + EXPECT_THAT(path.c_str(), StrEq("test\\foo\\bar")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, VectorConstructor) { + Path path; + std::vector directories; + + // Directories with no slashes. + directories = {"test", "foo", "bar"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with extraneous slashes. + directories = {"/test/", "/foo", "bar/"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string. + directories = {"test/foo", "bar"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string with extraneous slashes. + directories = {"/test/", "/foo/bar/"}; + path = Path(directories); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, VectorIteratorConstructor) { + Path path; + std::vector directories; + + // Directories with no slashes. + directories = {"test", "foo", "bar"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with extraneous slashes. + directories = {"/test/", "/foo", "bar/"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string. + directories = {"test/foo", "bar"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Multiple directories being added in one string with extraneous slashes. + directories = {"/test/", "/foo/bar/"}; + path = Path(directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with no slashes, starting from the second element. + directories = {"test", "foo", "bar"}; + path = Path(++directories.begin(), directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("foo/bar")); + EXPECT_FALSE(path.empty()); + + // Directories with no slashes, ending before the last element. + directories = {"test", "foo", "bar"}; + path = Path(directories.begin(), --directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + // Directories with no slashes, starting from the second element and ending + // before the last element. + directories = {"test", "foo", "bar"}; + path = Path(++directories.begin(), --directories.end()); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("foo")); + EXPECT_THAT(path.c_str(), StrEq("foo")); + EXPECT_FALSE(path.empty()); + + // Starting and ending at the sample place. + directories = {"test", "foo", "bar"}; + path = Path(directories.begin(), directories.begin()); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); +} + +TEST(PathTests, GetParent) { + Path path; + + path = Path("/test/foo/bar"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo")); + EXPECT_THAT(path.GetBaseName(), StrEq("bar")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar")); + EXPECT_FALSE(path.empty()); + + path = path.GetParent(); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + path = path.GetParent(); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test")); + EXPECT_THAT(path.str(), StrEq("test")); + EXPECT_THAT(path.c_str(), StrEq("test")); + EXPECT_FALSE(path.empty()); + + path = path.GetParent(); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("")); + EXPECT_THAT(path.str(), StrEq("")); + EXPECT_THAT(path.c_str(), StrEq("")); + EXPECT_TRUE(path.empty()); +} + +TEST(PathTests, GetChildWithString) { + Path path; + + path = path.GetChild("test"); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test")); + EXPECT_THAT(path.str(), StrEq("test")); + EXPECT_THAT(path.c_str(), StrEq("test")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild("foo"); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild("bar/baz"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.GetBaseName(), StrEq("baz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild("///quux///quaaz///"); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar/baz/quux")); + EXPECT_THAT(path.GetBaseName(), StrEq("quaaz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, GetChildWithPath) { + Path path; + + path = path.GetChild(Path("test")); + EXPECT_THAT(path.GetParent().str(), StrEq("")); + EXPECT_THAT(path.GetBaseName(), StrEq("test")); + EXPECT_THAT(path.str(), StrEq("test")); + EXPECT_THAT(path.c_str(), StrEq("test")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild(Path("foo")); + EXPECT_THAT(path.GetParent().str(), StrEq("test")); + EXPECT_THAT(path.GetBaseName(), StrEq("foo")); + EXPECT_THAT(path.str(), StrEq("test/foo")); + EXPECT_THAT(path.c_str(), StrEq("test/foo")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild(Path("bar/baz")); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar")); + EXPECT_THAT(path.GetBaseName(), StrEq("baz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz")); + EXPECT_FALSE(path.empty()); + + path = path.GetChild(Path("///quux///quaaz///")); + EXPECT_THAT(path.GetParent().str(), StrEq("test/foo/bar/baz/quux")); + EXPECT_THAT(path.GetBaseName(), StrEq("quaaz")); + EXPECT_THAT(path.str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_THAT(path.c_str(), StrEq("test/foo/bar/baz/quux/quaaz")); + EXPECT_FALSE(path.empty()); +} + +TEST(PathTests, IsParent) { + Path path("foo/bar/baz"); + + EXPECT_TRUE(Path().IsParent(Path())); + + EXPECT_TRUE(Path().IsParent(path)); + EXPECT_TRUE(Path("foo").IsParent(path)); + EXPECT_TRUE(Path("foo/").IsParent(path)); + EXPECT_TRUE(Path("foo/bar").IsParent(path)); + EXPECT_TRUE(Path("foo/bar/").IsParent(path)); + EXPECT_TRUE(Path("foo/bar/baz").IsParent(path)); + EXPECT_TRUE(Path("foo/bar/baz/").IsParent(path)); + EXPECT_TRUE(path.IsParent(Path("foo/bar/baz"))); + EXPECT_TRUE(path.IsParent(Path("foo/bar/baz/"))); + EXPECT_FALSE(path.IsParent(Path("foo"))); + EXPECT_FALSE(path.IsParent(Path("foo/"))); + EXPECT_FALSE(path.IsParent(Path("foo/bar"))); + EXPECT_FALSE(path.IsParent(Path("foo/bar/"))); + + EXPECT_FALSE(Path("completely/wrong").IsParent(path)); + EXPECT_FALSE(Path("f").IsParent(path)); + EXPECT_FALSE(Path("fo").IsParent(path)); + EXPECT_FALSE(Path("foo/b").IsParent(path)); + EXPECT_FALSE(Path("foo/ba").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/b").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/ba").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/baz/q").IsParent(path)); + EXPECT_FALSE(Path("foo/bar/baz/quux").IsParent(path)); +} + +TEST(PathTests, GetDirectories) { + std::vector golden = {"foo", "bar", "baz"}; + Path path; + + path = Path("foo/bar/baz"); + EXPECT_THAT(path.GetDirectories(), Eq(golden)); + + path = Path("//foo/bar///baz///"); + EXPECT_THAT(path.GetDirectories(), Eq(golden)); +} + +TEST(PathTests, FrontDirectory) { + EXPECT_EQ(Path().FrontDirectory(), Path()); + EXPECT_EQ(Path("single_level").FrontDirectory(), Path("single_level")); + EXPECT_EQ(Path("multi/level/directory/structure").FrontDirectory(), + Path("multi")); +} + +TEST(PathTests, PopFrontDirectory) { + EXPECT_EQ(Path().PopFrontDirectory(), Path()); + EXPECT_EQ(Path("single_level").PopFrontDirectory(), Path()); + EXPECT_EQ(Path("multi/level/directory/structure").PopFrontDirectory(), + Path("level/directory/structure")); +} + +TEST(PathTests, GetRelative) { + Path result; + + EXPECT_TRUE( + Path::GetRelative(Path(""), Path("starting/from/empty/path"), &result)); + EXPECT_EQ(result, Path("starting/from/empty/path")); + + EXPECT_TRUE(Path::GetRelative(Path("a/b/c/d/e"), + Path("a/b/c/d/e/f/g/h/i/j/k"), &result)); + EXPECT_THAT(result.str(), StrEq("f/g/h/i/j/k")); + + EXPECT_TRUE(Path::GetRelative( + Path("first_star/on_left"), + Path("first_star/on_left/straight_on/till_morning"), &result)); + EXPECT_THAT(result.str(), StrEq("straight_on/till_morning")); + + result = Path("result/left/untouched"); + + EXPECT_FALSE(Path::GetRelative(Path("some/overlap/but/failure"), + Path("some/overlap/and/unsuccessful"), + &result)); + EXPECT_THAT(result.str(), StrEq("result/left/untouched")); + + EXPECT_FALSE(Path::GetRelative(Path("no/overlap/at/all"), + Path("apple/banana/carrot"), &result)); + EXPECT_THAT(result.str(), StrEq("result/left/untouched")); + + EXPECT_FALSE(Path::GetRelative(Path("the/longer/path/comes/first/now"), + Path("the/longer/path"), &result)); + EXPECT_THAT(result.str(), StrEq("result/left/untouched")); +} + +TEST(PathTests, GetRelativeOptional) { + Optional result; + + result = Path::GetRelative(Path(""), Path("starting/from/empty/path")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, Path("starting/from/empty/path")); + + result = Path::GetRelative(Path("a/b/c/d/e"), Path("a/b/c/d/e/f/g/h/i/j/k")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, Path("f/g/h/i/j/k")); + + result = + Path::GetRelative(Path("first_star/on_left"), + Path("first_star/on_left/straight_on/till_morning")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, Path("straight_on/till_morning")); + + result = Path::GetRelative(Path("some/overlap/but/failure"), + Path("some/overlap/and/unsuccessful")); + EXPECT_FALSE(result.has_value()); + + result = + Path::GetRelative(Path("no/overlap/at/all"), Path("apple/banana/carrot")); + EXPECT_FALSE(result.has_value()); + + result = Path::GetRelative(Path("the/longer/path/comes/first/now"), + Path("the/longer/path")); + EXPECT_FALSE(result.has_value()); +} + +} // namespace diff --git a/app/tests/reference_count_test.cc b/app/tests/reference_count_test.cc new file mode 100644 index 0000000000..623f1a5ef3 --- /dev/null +++ b/app/tests/reference_count_test.cc @@ -0,0 +1,275 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/reference_count.h" + +#include "app/src/mutex.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::IsNull; + +using ::firebase::internal::ReferenceCount; +using ::firebase::internal::ReferenceCountedInitializer; +using ::firebase::internal::ReferenceCountLock; + +class ReferenceCountTest : public ::testing::Test { + protected: + ReferenceCount count_; +}; + +TEST_F(ReferenceCountTest, Construct) { + EXPECT_THAT(count_.references(), Eq(0)); +} + +TEST_F(ReferenceCountTest, AddReference) { + EXPECT_THAT(count_.AddReference(), Eq(0)); + EXPECT_THAT(count_.references(), Eq(1)); + EXPECT_THAT(count_.AddReference(), Eq(1)); + EXPECT_THAT(count_.references(), Eq(2)); +} + +TEST_F(ReferenceCountTest, RemoveReference) { + count_.AddReference(); + count_.AddReference(); + EXPECT_THAT(count_.RemoveReference(), Eq(2)); + EXPECT_THAT(count_.references(), Eq(1)); + EXPECT_THAT(count_.RemoveReference(), Eq(1)); + EXPECT_THAT(count_.references(), Eq(0)); + EXPECT_THAT(count_.RemoveReference(), Eq(0)); + EXPECT_THAT(count_.references(), Eq(0)); +} + +TEST_F(ReferenceCountTest, RemoveAllReferences) { + count_.AddReference(); + count_.AddReference(); + EXPECT_THAT(count_.RemoveAllReferences(), Eq(2)); + EXPECT_THAT(count_.references(), Eq(0)); +} + +class ReferenceCountLockTest : public ::testing::Test { + protected: + void SetUp() override { count_.AddReference(); } + + protected: + ReferenceCount count_; +}; + +TEST_F(ReferenceCountLockTest, Construct) { + { + ReferenceCountLock lock(&count_); + EXPECT_THAT(lock.references(), Eq(1)); + EXPECT_THAT(count_.references(), Eq(2)); + } + EXPECT_THAT(count_.references(), Eq(1)); +} + +TEST_F(ReferenceCountLockTest, AddReference) { + ReferenceCountLock lock(&count_); + EXPECT_THAT(lock.references(), Eq(1)); + EXPECT_THAT(lock.AddReference(), Eq(1)); + EXPECT_THAT(lock.references(), Eq(2)); +} + +TEST_F(ReferenceCountLockTest, RemoveReference) { + ReferenceCountLock lock(&count_); + lock.AddReference(); + lock.AddReference(); + EXPECT_THAT(lock.RemoveReference(), Eq(3)); + EXPECT_THAT(lock.references(), Eq(2)); + EXPECT_THAT(lock.RemoveReference(), Eq(2)); + EXPECT_THAT(lock.references(), Eq(1)); + EXPECT_THAT(lock.RemoveReference(), Eq(1)); + EXPECT_THAT(lock.references(), Eq(0)); + EXPECT_THAT(lock.RemoveReference(), Eq(0)); + EXPECT_THAT(lock.references(), Eq(0)); +} + +TEST_F(ReferenceCountLockTest, RemoveAllReferences) { + ReferenceCountLock lock(&count_); + lock.AddReference(); + EXPECT_THAT(lock.references(), Eq(2)); + EXPECT_THAT(lock.RemoveAllReferences(), Eq(2)); + EXPECT_THAT(lock.references(), Eq(0)); + EXPECT_THAT(count_.references(), Eq(0)); +} + +class ReferenceCountedInitializerTest : public ::testing::Test { + protected: + // Object to initialize in Initialize(). + struct Context { + bool initialize_success; + int initialized_count; + }; + + protected: + // Initialize the context object. + static bool Initialize(Context* context) { + if (!context->initialize_success) return false; + context->initialized_count++; + return true; + } + + static void Terminate(Context* context) { context->initialized_count--; } +}; + +TEST_F(ReferenceCountedInitializerTest, ConstructEmpty) { + ReferenceCountedInitializer initializer; + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(initializer.initialize(), IsNull()); + EXPECT_THAT(initializer.terminate(), IsNull()); + EXPECT_THAT(initializer.context(), IsNull()); + // Use the mutex accessor to instantiate the template. + firebase::MutexLock lock(initializer.mutex()); +} + +TEST_F(ReferenceCountedInitializerTest, ConstructWithTerminate) { + Context context; + ReferenceCountedInitializer initializer(Terminate, &context); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(initializer.initialize(), IsNull()); + EXPECT_THAT(initializer.terminate(), Eq(Terminate)); + EXPECT_THAT(initializer.context(), Eq(&context)); +} + +TEST_F(ReferenceCountedInitializerTest, ConstructWithInitializeAndTerminate) { + Context context; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(initializer.initialize(), Eq(Initialize)); + EXPECT_THAT(initializer.terminate(), Eq(Terminate)); + EXPECT_THAT(initializer.context(), Eq(&context)); +} + +TEST_F(ReferenceCountedInitializerTest, SetContext) { + Context context; + ReferenceCountedInitializer initializer(Initialize, Terminate, + nullptr); + EXPECT_THAT(initializer.context(), IsNull()); + initializer.set_context(&context); + EXPECT_THAT(initializer.context(), Eq(&context)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceNoInit) { + ReferenceCountedInitializer initializer(nullptr, nullptr, nullptr); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceInlineInit) { + Context context = {true, 0}; + ReferenceCountedInitializer initializer; + EXPECT_THAT(initializer.AddReference( + [](Context* state) { + state->initialized_count = 12345678; + return true; + }, + &context), + Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(12345678)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceSuccessfulInit) { + Context context = {true, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(1)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); + EXPECT_THAT(context.initialized_count, Eq(1)); +} + +TEST_F(ReferenceCountedInitializerTest, AddReferenceFailedInit) { + Context context = {false, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(-1)); + EXPECT_THAT(initializer.references(), Eq(0)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveReferenceNoInit) { + Context context = {true, 3}; + ReferenceCountedInitializer initializer(nullptr, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(3)); + EXPECT_THAT(initializer.RemoveReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(2)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveAllReferences) { + Context context = {true, 3}; + ReferenceCountedInitializer initializer(nullptr, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(3)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); + EXPECT_THAT(initializer.RemoveAllReferences(), Eq(2)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(2)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveAllReferencesWithoutTerminate) { + Context context = {true, 3}; + ReferenceCountedInitializer initializer(nullptr, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(3)); + EXPECT_THAT(initializer.AddReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(2)); + EXPECT_THAT(initializer.RemoveAllReferencesWithoutTerminate(), Eq(2)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(3)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveReferenceSuccessfulInit) { + Context context = {true, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(1)); + EXPECT_THAT(context.initialized_count, Eq(1)); + EXPECT_THAT(initializer.RemoveReference(), Eq(1)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); + EXPECT_THAT(initializer.RemoveReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); +} + +TEST_F(ReferenceCountedInitializerTest, RemoveReferenceFailedInit) { + Context context = {false, 0}; + ReferenceCountedInitializer initializer(Initialize, Terminate, + &context); + EXPECT_THAT(initializer.AddReference(), Eq(-1)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); + EXPECT_THAT(initializer.RemoveReference(), Eq(0)); + EXPECT_THAT(initializer.references(), Eq(0)); + EXPECT_THAT(context.initialized_count, Eq(0)); +} diff --git a/app/tests/scheduler_test.cc b/app/tests/scheduler_test.cc new file mode 100644 index 0000000000..3a9baaf123 --- /dev/null +++ b/app/tests/scheduler_test.cc @@ -0,0 +1,369 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/scheduler.h" +#include "app/memory/atomic.h" +#include "app/src/semaphore.h" +#include "app/src/time.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace scheduler { + +using ::testing::Eq; + +class SchedulerTest : public ::testing::Test { + protected: + SchedulerTest() {} + + void SetUp() override { + atomic_count_.store(0); + while (callback_sem1_.TryWait()) {} + while (callback_sem2_.TryWait()) {} + ordered_value_.clear(); + repeat_period_ms_ = 0; + repeat_countdown_ = 0; + } + + static void SemaphorePost1() { + callback_sem1_.Post(); + } + + static void AddCount() { + atomic_count_.fetch_add(1); + callback_sem1_.Post(); + } + + static void AddValueInOrder(int v) { + ordered_value_.push_back(v); + callback_sem1_.Post(); + } + + static void RecursiveCallback(Scheduler* scheduler) { + callback_sem1_.Post(); + --repeat_countdown_; + + if (repeat_countdown_ > 0) { + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, RecursiveCallback), + repeat_period_ms_); + } + } + + static compat::Atomic atomic_count_; + static Semaphore callback_sem1_; + static Semaphore callback_sem2_; + static std::vector ordered_value_; + static int repeat_period_ms_; + static int repeat_countdown_; + + Scheduler scheduler_; +}; + +compat::Atomic SchedulerTest::atomic_count_(0); +Semaphore SchedulerTest::callback_sem1_(0); // NOLINT +Semaphore SchedulerTest::callback_sem2_(0); // NOLINT +std::vector SchedulerTest::ordered_value_; // NOLINT +int SchedulerTest::repeat_period_ms_ = 0; +int SchedulerTest::repeat_countdown_ = 0; + +// 10000 seems to be a good number to surface racing condition. +const int kThreadTestIteration = 10000; + +TEST_F(SchedulerTest, Basic) { + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1)); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1), 1); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); +} + +#ifdef FIREBASE_USE_STD_FUNCTION +TEST_F(SchedulerTest, BasicStdFunction) { + std::function func = [this](){ + callback_sem1_.Post(); + }; + + scheduler_.Schedule(func); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + + scheduler_.Schedule(func, 1); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); +} +#endif + +TEST_F(SchedulerTest, TriggerOrderNoDelay) { + std::vector expected; + for (int i = 0; i < kThreadTestIteration; ++i) + { + scheduler_.Schedule( + new callback::CallbackValue1( + i, AddValueInOrder)); + expected.push_back(i); + } + + for (int i = 0; i < kThreadTestIteration; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } + EXPECT_THAT(ordered_value_, Eq(expected)); +} + +TEST_F(SchedulerTest, TriggerOrderSameDelay) { + std::vector expected; + for (int i = 0; i < kThreadTestIteration; ++i) + { + scheduler_.Schedule( + new callback::CallbackValue1( + i, AddValueInOrder), 1); + expected.push_back(i); + } + + for (int i = 0; i < kThreadTestIteration; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } + EXPECT_THAT(ordered_value_, Eq(expected)); +} + +TEST_F(SchedulerTest, TriggerOrderDifferentDelay) { + std::vector expected; + for (int i = 0; i < 1000; ++i) + { + scheduler_.Schedule( + new callback::CallbackValue1( + i, AddValueInOrder), i); + expected.push_back(i); + } + + for (int i = 0; i < 1000; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(2000)); + } + + EXPECT_THAT(ordered_value_, Eq(expected)); +} + +TEST_F(SchedulerTest, ExecuteDuringCallback) { + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, [](Scheduler* scheduler){ + callback_sem1_.Post(); + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, [](Scheduler* scheduler){ + callback_sem2_.Post(); + })); + })); + + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(callback_sem2_.TimedWait(1000)); +} + +TEST_F(SchedulerTest, ScheduleDuringCallback1) { + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, [](Scheduler* scheduler){ + callback_sem1_.Post(); + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, [](Scheduler* scheduler){ + callback_sem2_.Post(); + }), 1); + }), 1); + + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(callback_sem2_.TimedWait(1000)); +} + +TEST_F(SchedulerTest, ScheduleDuringCallback100) { + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, [](Scheduler* scheduler){ + callback_sem1_.Post(); + scheduler->Schedule( + new callback::CallbackValue1( + scheduler, [](Scheduler* scheduler){ + callback_sem2_.Post(); + }), 100); + }), 100); + + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(callback_sem2_.TimedWait(1000)); +} + +TEST_F(SchedulerTest, RecursiveCallbackNoInterval) { + repeat_period_ms_ = 0; + repeat_countdown_ = 1000; + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, RecursiveCallback), + repeat_period_ms_); + + for (int i = 0; i < 1000; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, RecursiveCallbackWithInterval) { + repeat_period_ms_ = 10; + repeat_countdown_ = 5; + scheduler_.Schedule( + new callback::CallbackValue1( + &scheduler_, RecursiveCallback), + repeat_period_ms_); + + for (int i = 0; i < 5; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, RepeatCallbackNoDelay) { + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1), 0, 1); + + // Wait for it to repeat 100 times + for (int i = 0; i < 100; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, RepeatCallbackWithDelay) { + int delay = 100; + scheduler_.Schedule(new callback::CallbackVoid(SemaphorePost1), delay, 1); + + auto start = internal::GetTimestamp(); + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + auto end = internal::GetTimestamp(); + + // Test if the first delay actually works. + int actual_delay = static_cast(end - start); + int error = abs(actual_delay - delay); + printf("Delay: %dms. Actual delay: %dms. Error: %dms\n", delay, actual_delay, + error); + EXPECT_TRUE(error < 0.1 * internal::kMillisecondsPerSecond); + + // Wait for it to repeat 100 times + for (int i = 0; i < 100; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + } +} + +TEST_F(SchedulerTest, CancelImmediateCallback) { + auto test_func = [](int delay){ + // Use standalone scheduler and counter + Scheduler scheduler; + compat::Atomic count(0); + int success_cancel = 0; + for (int i = 0; i < kThreadTestIteration; ++i) { + bool cancelled = scheduler.Schedule( + new callback::CallbackValue1*>( + &count, [](compat::Atomic* count){ + count->fetch_add(1); + }), 0).Cancel(); + if (cancelled) { + ++success_cancel; + } + } + + internal::Sleep(10); + + // Does not guarantee 100% successful cancellation + float success_rate = success_cancel * 100.0f / kThreadTestIteration; + printf("[Delay %dms] Cancel success rate: %.1f%% (And it is ok if not 100%%" + ")\n", delay, success_rate); + EXPECT_THAT(success_cancel + count.load(), + Eq(kThreadTestIteration)); + }; + + // Test without delay + test_func(0); + + // Test with delay + test_func(1); +} + +// This test can take around 5s ~ 30s depending on the platform +TEST_F(SchedulerTest, CancelRepeatCallback) { + auto test_func = [](int delay, int repeat, int wait_repeat){ + // Use standalone scheduler and counter for iterations + Scheduler scheduler; + compat::Atomic count(0); + while (callback_sem1_.TryWait()) {} + + RequestHandle handler = + scheduler.Schedule(new callback::CallbackValue1*>( + &count, [](compat::Atomic* count){ + count->fetch_add(1); + callback_sem1_.Post(); + }), delay, repeat); + EXPECT_FALSE(handler.IsCancelled()); + + for (int i = 0; i < wait_repeat; ++i) { + EXPECT_TRUE(callback_sem1_.TimedWait(1000)); + EXPECT_TRUE(handler.IsTriggered()); + } + + // Cancellation of a repeat cb should always be successful, as long as + // it is not cancelled yet + EXPECT_TRUE(handler.Cancel()); + EXPECT_TRUE(handler.IsCancelled()); + EXPECT_FALSE(handler.Cancel()); + + // Should have no more cb triggered after the cancellation + int saved_count = count.load(); + + internal::Sleep(1); + EXPECT_THAT(count.load(), Eq(saved_count)); + }; + + for (int i = 0; i < 1000; ++i) { + // No delay and do not wait for the first trigger to cancel it + test_func(0, 1, 0); + // No delay and wait for the first trigger, then cancel it + test_func(0, 1, 1); + // 1ms delay and do not wait for the first trigger to cancel it + test_func(1, 1, 0); + // 1ms delay and wait for the first trigger, then cancel it + test_func(1, 1, 1); + } +} + +TEST_F(SchedulerTest, CancelAll) { + Scheduler scheduler; + for (int i = 0; i < kThreadTestIteration; ++i) { + scheduler.Schedule(new callback::CallbackVoid(AddCount)); + } + scheduler.CancelAllAndShutdownWorkerThread(); + // Does not guarantee 0% trigger rate + float trigger_rate = atomic_count_.load() * 100.0f / kThreadTestIteration; + printf("Callback trigger rate: %.1f%% (And it is ok if not 0%%)\n", + trigger_rate); +} + +TEST_F(SchedulerTest, DeleteScheduler) { + for (int i = 0; i < kThreadTestIteration; ++i) { + Scheduler scheduler; + scheduler.Schedule(new callback::CallbackVoid(AddCount)); + } + + // Does not guarantee 0% trigger rate + float trigger_rate = atomic_count_.load() * 100.0f / kThreadTestIteration; + printf("Callback trigger rate: %.1f%% (And it is ok if not 0%%)\n", + trigger_rate); +} + +} // namespace scheduler +} // namespace firebase diff --git a/app/tests/secure/user_secure_integration_test.cc b/app/tests/secure/user_secure_integration_test.cc new file mode 100644 index 0000000000..7b6a851a46 --- /dev/null +++ b/app/tests/secure/user_secure_integration_test.cc @@ -0,0 +1,255 @@ +// Copyright 2019 Google LLC +// +// 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 // NOLINT +#include + +#include "app/src/secure/user_secure_internal.h" +#include "app/src/secure/user_secure_manager.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif // __APPLE__ + +// If FORCE_FAKE_SECURE_STORAGE is defined, force usage of fake (non-secure) +// storage, suitable for testing only, NOT for production use. Otherwise, use +// the default secure storage type for each platform, except on Linux if not +// running locally, which also forces fake storage (as libsecret requires that +// you are running locally), or on unknown other platforms (as there is no +// platform-independent secure storage solution). + +#if !defined(FORCE_FAKE_SECURE_STORAGE) +#if defined(_WIN32) +#include "app/src/secure/user_secure_windows_internal.h" +#define USER_SECURE_TYPE UserSecureWindowsInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(TARGET_OS_OSX) && TARGET_OS_OSX +#include "app/src/secure/user_secure_darwin_internal.h" +#include "app/src/secure/user_secure_darwin_internal_testlib.h" +#define USER_SECURE_TYPE UserSecureDarwinInternal +#define USER_SECURE_TEST_HELPER UserSecureDarwinTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(__linux__) && defined(USER_SECURE_LOCAL_TEST) +#include "app/src/secure/user_secure_linux_internal.h" +#define USER_SECURE_TYPE UserSecureLinuxInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#else // Unknown platform, or linux test running non-locally, use fake version. +#define FORCE_FAKE_SECURE_STORAGE +#endif // platform selector +#endif // !defined(FORCE_FAKE_SECURE_STORAGE) + +#ifdef FORCE_FAKE_SECURE_STORAGE +#include "app/src/secure/user_secure_fake_internal.h" +#define USER_SECURE_TYPE UserSecureFakeInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE GetTestTmpDir(kTestNameSpaceShort).c_str() +#if defined(_WIN32) +// For GetEnvironmentVariable to read TEST_TEMPDIR. +#include +#else +#include +#endif // defined(_WIN32) +#endif // FORCE_FAKE_SECURE_STORAGE + +namespace firebase { +namespace app { +namespace secure { + +using ::testing::Eq; +using ::testing::StrEq; + +class UserSecureEmptyTestHelper {}; + +#if defined(_WIN32) +static const char kDirectorySeparator[] = "\\"; +#else +static const char kDirectorySeparator[] = "/"; +#endif // defined(_WIN32) + +static std::string GetTestTmpDir(const char test_namespace[]) { +#if defined(_WIN32) + char buf[MAX_PATH + 1]; + if (GetEnvironmentVariable("TEST_TMPDIR", buf, sizeof(buf))) { + return std::string(buf) + kDirectorySeparator + test_namespace; + } +#else + // Linux and OS X should either have the TEST_TMPDIR environment variable set. + if (const char* value = getenv("TEST_TMPDIR")) { + return std::string(value) + kDirectorySeparator + test_namespace; + } +#endif // defined(_WIN32) + // If we weren't able to get TEST_TMPDIR, just use a subdirectory. + return test_namespace; +} + +// test app name and data +const char kAppName1[] = "app1"; +const char kUserData1[] = "123456"; +const char kAppName2[] = "app2"; +const char kUserData2[] = "654321"; + +const char kDomain[] = "integration_test"; + +// NOLINTNEXTLINE +const char kTestNameSpace[] = "com.google.firebase.TestKeys"; +// NOLINTNEXTLINE +const char kTestNameSpaceShort[] = "firebase_test"; + +class UserSecureTest : public ::testing::Test { + protected: + void SetUp() override { + user_secure_test_helper_ = MakeUnique(); + UserSecureInternal* internal = + new USER_SECURE_TYPE(kDomain, USER_SECURE_TEST_NAMESPACE); + UniquePtr user_secure_ptr(internal); + manager_ = new UserSecureManager(std::move(user_secure_ptr)); + CleanUpTestData(); + } + + void TearDown() override { + CleanUpTestData(); + delete manager_; + } + + void CleanUpTestData() { + Future delete_all_future = manager_->DeleteAllData(); + WaitForResponse(delete_all_future); + user_secure_test_helper_ = nullptr; + } + + // Busy waits until |response_future| has completed. + void WaitForResponse(const FutureBase& response_future) { + while (true) { + if (response_future.status() != FutureStatus::kFutureStatusPending) { + break; + } + } + } + + UserSecureManager* manager_; + UniquePtr user_secure_test_helper_; +}; + +TEST_F(UserSecureTest, NoData) { + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kNoEntry); + EXPECT_THAT(*(load_future.result()), StrEq("")); +} + +TEST_F(UserSecureTest, SetDataGetData) { + // Add Data + Future save_future = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future); + EXPECT_THAT(save_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future.error(), kSuccess); + // Check the added key for correctness + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kSuccess); + std::string originalString(kUserData1); + EXPECT_THAT(*(load_future.result()), StrEq(originalString)); +} + +TEST_F(UserSecureTest, SetDataDeleteDataGetNoData) { + // Add Data + Future save_future = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future); + EXPECT_THAT(save_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future.error(), kSuccess); + // Delete Data + Future delete_future = manager_->DeleteUserData(kAppName1); + WaitForResponse(delete_future); + EXPECT_THAT(delete_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(delete_future.error(), kSuccess); + // Check data empty + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kNoEntry); + EXPECT_THAT(*(load_future.result()), StrEq("")); +} + +TEST_F(UserSecureTest, SetTwoDataDeleteOneGetData) { + // Add Data1 + Future save_future1 = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future1); + EXPECT_THAT(save_future1.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future1.error(), kSuccess); + // Add Data2 + Future save_future2 = manager_->SaveUserData(kAppName2, kUserData2); + WaitForResponse(save_future2); + EXPECT_THAT(save_future2.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future2.error(), kSuccess); + + // Delete Data1 + Future delete_future = manager_->DeleteUserData(kAppName1); + WaitForResponse(delete_future); + EXPECT_THAT(delete_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(delete_future.error(), kSuccess); + + // Check the data2 + Future load_future = manager_->LoadUserData(kAppName2); + WaitForResponse(load_future); + EXPECT_THAT(load_future.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future.error(), kSuccess); + std::string originalString(kUserData2); + EXPECT_THAT(*(load_future.result()), StrEq(originalString)); +} + +TEST_F(UserSecureTest, CheckDeleteAll) { + // Add Data1 + Future save_future1 = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future1); + EXPECT_THAT(save_future1.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future1.error(), kSuccess); + // Add Data2 + Future save_future2 = manager_->SaveUserData(kAppName2, kUserData2); + WaitForResponse(save_future2); + EXPECT_THAT(save_future2.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(save_future2.error(), kSuccess); + + // Delete all data + Future delete_all_future = manager_->DeleteAllData(); + WaitForResponse(delete_all_future); + EXPECT_THAT(delete_all_future.status(), + Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(delete_all_future.error(), kSuccess); + // Check data1 empty + Future load_future1 = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future1); + EXPECT_THAT(load_future1.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future1.error(), kNoEntry); + EXPECT_THAT(*(load_future1.result()), StrEq("")); + + // Check data2 empty + Future load_future2 = manager_->LoadUserData(kAppName2); + WaitForResponse(load_future2); + EXPECT_THAT(load_future2.status(), Eq(FutureStatus::kFutureStatusComplete)); + EXPECT_THAT(load_future2.error(), kNoEntry); + EXPECT_THAT(*(load_future2.result()), StrEq("")); +} + +} // namespace secure +} // namespace app +} // namespace firebase diff --git a/app/tests/secure/user_secure_internal_test.cc b/app/tests/secure/user_secure_internal_test.cc new file mode 100644 index 0000000000..42f6a9c192 --- /dev/null +++ b/app/tests/secure/user_secure_internal_test.cc @@ -0,0 +1,285 @@ +// Copyright 2019 Google LLC +// +// 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 "gtest/gtest.h" +#include "gmock/gmock.h" + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif // __APPLE__ + +// If FORCE_FAKE_SECURE_STORAGE is defined, force usage of fake (non-secure) +// storage, suitable for testing only, NOT for production use. Otherwise, use +// the default secure storage type for each platform, except on Linux if not +// running locally, which also forces fake storage (as libsecret requires that +// you are running locally), or on unknown other platforms (as there is no +// platform-independent secure storage solution). + +#if !defined(FORCE_FAKE_SECURE_STORAGE) +#if defined(_WIN32) +#include "app/src/secure/user_secure_windows_internal.h" +#define USER_SECURE_TYPE UserSecureWindowsInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(TARGET_OS_OSX) && TARGET_OS_OSX +#include "app/src/secure/user_secure_darwin_internal.h" +#include "app/src/secure/user_secure_darwin_internal_testlib.h" +#define USER_SECURE_TYPE UserSecureDarwinInternal +#define USER_SECURE_TEST_HELPER UserSecureDarwinTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#elif defined(__linux__) && defined(USER_SECURE_LOCAL_TEST) +#include "app/src/secure/user_secure_linux_internal.h" +#define USER_SECURE_TYPE UserSecureLinuxInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE kTestNameSpace + +#else // Unknown platform, or linux test running non-locally, use fake version. +#define FORCE_FAKE_SECURE_STORAGE +#endif // platform selector +#endif // !defined(FORCE_FAKE_SECURE_STORAGE) + +#ifdef FORCE_FAKE_SECURE_STORAGE +#include "app/src/secure/user_secure_fake_internal.h" +#define USER_SECURE_TYPE UserSecureFakeInternal +#define USER_SECURE_TEST_HELPER UserSecureEmptyTestHelper +#define USER_SECURE_TEST_NAMESPACE GetTestTmpDir(kTestNameSpaceShort).c_str() +#if defined(_WIN32) +// For GetEnvironmentVariable to read TEST_TEMPDIR. +#include +#else +#include +#endif // defined(_WIN32) +#endif // FORCE_FAKE_SECURE_STORAGE + +namespace firebase { +namespace app { +namespace secure { + +using ::testing::StrEq; + +class UserSecureEmptyTestHelper {}; + +#if defined(_WIN32) +static const char kDirectorySeparator[] = "\\"; +#else +static const char kDirectorySeparator[] = "/"; +#endif // defined(_WIN32) + +static std::string GetTestTmpDir(const char test_namespace[]) { +#if defined(_WIN32) + char buf[MAX_PATH + 1]; + if (GetEnvironmentVariable("TEST_TMPDIR", buf, sizeof(buf))) { + return std::string(buf) + kDirectorySeparator + test_namespace; + } +#else + // Linux and OS X should either have the TEST_TMPDIR environment variable set. + if (const char* value = getenv("TEST_TMPDIR")) { + return std::string(value) + kDirectorySeparator + test_namespace; + } +#endif // defined(_WIN32) + // If we weren't able to get TEST_TMPDIR, just use a subdirectory. + return test_namespace; +} + +// test app name and data +const char kAppName1[] = "app1"; +const char kUserData1[] = "123456"; +const char kUserData1Alt[] = "12345ABC"; +const char kUserData1ReAdd[] = "123456789"; +const char kAppName2[] = "app2"; +const char kUserData2[] = "654321"; +const char kAppNameNoExist[] = "app_no_exist"; + +const char kDomain[] = "internal_test"; + +// NOLINTNEXTLINE +const char kTestNameSpace[] = "com.google.firebase.TestKeys"; +// NOLINTNEXTLINE +const char kTestNameSpaceShort[] = "firebase_test"; + +class UserSecureInternalTest : public ::testing::Test { + protected: + void SetUp() override { + user_secure_test_helper_ = MakeUnique(); + user_secure_ = + MakeUnique(kDomain, USER_SECURE_TEST_NAMESPACE); + CleanUpTestData(); + } + + void TearDown() override { + CleanUpTestData(); + user_secure_ = nullptr; + user_secure_test_helper_ = nullptr; + } + + void CleanUpTestData() { user_secure_->DeleteAllData(); } + + UniquePtr user_secure_; + UniquePtr user_secure_test_helper_; +}; + +TEST_F(UserSecureInternalTest, NoData) { + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetDataGetData) { + // Add Data + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check the added key for correctness + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); +} + +TEST_F(UserSecureInternalTest, SetDataDeleteDataGetNoData) { + // Add Data + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Delete Data + user_secure_->DeleteUserData(kAppName1); + // Check data empty + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetTwoDataDeleteOneGetData) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Add Data2 + user_secure_->SaveUserData(kAppName2, kUserData2); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); + // Check previous save is still valid. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Delete Data1 + user_secure_->DeleteUserData(kAppName1); + // Check the data2 + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); +} + +TEST_F(UserSecureInternalTest, CheckDeleteAll) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Add Data2 + user_secure_->SaveUserData(kAppName2, kUserData2); + // Check save succeed. + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); + // Delete all data + user_secure_->DeleteAllData(); + // Check data1 empty + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); + // Check data2 empty + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetGetAfterDeleteAll) { + // Delete all data + user_secure_->DeleteAllData(); + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check data1 correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); +} + +TEST_F(UserSecureInternalTest, AddOverride) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check data1 correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Override same key with Data1ReAdd. + user_secure_->SaveUserData(kAppName1, kUserData1ReAdd); + // Check Data1ReAdd correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1ReAdd)); +} + +TEST_F(UserSecureInternalTest, DeleteAndAddWithSameKey) { + // Add Data1 + user_secure_->SaveUserData(kAppName1, kUserData1); + // Check data1 correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + // Delete Data1 + user_secure_->DeleteUserData(kAppName1); + // Add Data1ReAdd to same key. + user_secure_->SaveUserData(kAppName1, kUserData1ReAdd); + // Check Data1ReAdd correctness. + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1ReAdd)); +} + +TEST_F(UserSecureInternalTest, DeleteKeyNotExist) { + // Delete Data1 + user_secure_->DeleteUserData(kAppNameNoExist); + // Check data1 empty + EXPECT_THAT(user_secure_->LoadUserData(kAppNameNoExist), StrEq("")); +} + +TEST_F(UserSecureInternalTest, SetLargeDataThenDeleteIt) { + // Set up a large buffer of data. + const size_t kSize = 20000; + char data[kSize]; + for (int i = 0; i < kSize - 1; ++i) { + data[i] = 'A' + (i % 26); + } + data[kSize - 1] = '\0'; + std::string user_data(data); + // Add Data + user_secure_->SaveUserData(kAppName1, user_data); + // Check the added key for correctness + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(user_data)); + // Check that we can delete the large data. + user_secure_->DeleteUserData(kAppName1); + // Check the added key for correctness + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq("")); +} + +TEST_F(UserSecureInternalTest, TestMultipleDomains) { + // Set up an alternate UserSecureInternal with a different domain. + UniquePtr alt_user_secure = MakeUnique( + "alternate_test", USER_SECURE_TEST_NAMESPACE); + alt_user_secure->DeleteAllData(); + + user_secure_->SaveUserData(kAppName1, kUserData1); + user_secure_->SaveUserData(kAppName2, kUserData2); + alt_user_secure->SaveUserData(kAppName1, kUserData1Alt); + + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)) + << "Modifying a key in alt_user_secure changed a key in user_secure_"; + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName1), StrEq(kUserData1Alt)); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName2), StrEq("")); + + // Ensure deleting data from one UserSecureInternal doesn't delete data in the + // other. + alt_user_secure->DeleteUserData(kAppName1); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName1), StrEq("")); + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + + alt_user_secure->SaveUserData(kAppName1, kUserData1Alt); + alt_user_secure->SaveUserData(kAppName2, kUserData2); + // Ensure deleting ALL data from one UserSecureInternal doesn't delete the + // other. + alt_user_secure->DeleteAllData(); + EXPECT_THAT(user_secure_->LoadUserData(kAppName1), StrEq(kUserData1)); + EXPECT_THAT(user_secure_->LoadUserData(kAppName2), StrEq(kUserData2)); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName1), StrEq("")); + EXPECT_THAT(alt_user_secure->LoadUserData(kAppName2), StrEq("")); +} + +} // namespace secure +} // namespace app +} // namespace firebase diff --git a/app/tests/secure/user_secure_manager_test.cc b/app/tests/secure/user_secure_manager_test.cc new file mode 100644 index 0000000000..d3f1864e08 --- /dev/null +++ b/app/tests/secure/user_secure_manager_test.cc @@ -0,0 +1,178 @@ +// Copyright 2019 Google LLC +// +// 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 "app/src/secure/user_secure_manager.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace app { +namespace secure { + +using ::testing::Ne; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::StrEq; + +const char kAppName1[] = "app_name_1"; +const char kUserData1[] = "123456"; + +TEST(UserSecureManager, Constructor) { + UniquePtr user_secure; + UserSecureManager manager(std::move(user_secure)); + + // Just making sure this constructor doesn't crash or leak memory. No further + // tests. +} + +class UserSecureInternalMock : public UserSecureInternal { + public: + MOCK_METHOD(std::string, LoadUserData, (const std::string& app_name), + (override)); + MOCK_METHOD(void, SaveUserData, + (const std::string& app_name, const std::string& user_data), + (override)); + MOCK_METHOD(void, DeleteUserData, (const std::string& app_name), (override)); + MOCK_METHOD(void, DeleteAllData, (), (override)); +}; + +class UserSecureManagerTest : public ::testing::Test { + public: + friend class UserSecureManager; + void SetUp() override { + user_secure_ = new testing::StrictMock(); + UniquePtr user_secure_ptr(user_secure_); + + manager_ = new UserSecureManager(std::move(user_secure_ptr)); + } + + void TearDown() override { delete manager_; } + + // Busy waits until |response_future| has completed. + void WaitForResponse(const FutureBase& response_future) { + ASSERT_THAT(response_future.status(), + Ne(FutureStatus::kFutureStatusInvalid)); + while (true) { + if (response_future.status() != FutureStatus::kFutureStatusPending) { + break; + } + } + } + + protected: + UserSecureInternalMock* user_secure_; + UserSecureManager* manager_; +}; + +TEST_F(UserSecureManagerTest, LoadUserData) { + EXPECT_CALL(*user_secure_, LoadUserData(kAppName1)) + .WillOnce(Return(kUserData1)); + Future load_future = manager_->LoadUserData(kAppName1); + WaitForResponse(load_future); + EXPECT_EQ(load_future.status(), FutureStatus::kFutureStatusComplete); + EXPECT_THAT(load_future.result(), Pointee(StrEq(kUserData1))); +} + +TEST_F(UserSecureManagerTest, SaveUserData) { + EXPECT_CALL(*user_secure_, SaveUserData(kAppName1, kUserData1)).Times(1); + Future save_future = manager_->SaveUserData(kAppName1, kUserData1); + WaitForResponse(save_future); + EXPECT_EQ(save_future.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(UserSecureManagerTest, DeleteUserData) { + EXPECT_CALL(*user_secure_, DeleteUserData(kAppName1)).Times(1); + Future delete_future = manager_->DeleteUserData(kAppName1); + WaitForResponse(delete_future); + EXPECT_EQ(delete_future.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(UserSecureManagerTest, DeleteAllData) { + EXPECT_CALL(*user_secure_, DeleteAllData()).Times(1); + Future delete_all_future = manager_->DeleteAllData(); + WaitForResponse(delete_all_future); + + EXPECT_EQ(delete_all_future.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(UserSecureManagerTest, TestHexEncodingAndDecoding) { + const char kBinaryData[] = + "\x00\x05\x20\x3C\x40\x45\x50\x60\x70\x80\x90\x00\xA0\xB5\xC2\xD1\xF0" + "\xFF\x00\xE0\x42"; + const char kBase64EncodedData[] = "#AAUgPEBFUGBwgJAAoLXC0fD/AOBC"; + const char kHexEncodedData[] = "$0005203C4045506070809000A0B5C2D1F0FF00E042"; + std::string binary_data(kBinaryData, sizeof(kBinaryData) - 1); + std::string encoded; + std::string decoded; + + UserSecureManager::BinaryToAscii(binary_data, &encoded); + // Ensure that the data was Base64-encoded. + EXPECT_EQ(encoded, kBase64EncodedData); + // Ensure the data decodes back to the original. + EXPECT_TRUE(UserSecureManager::AsciiToBinary(encoded, &decoded)); + EXPECT_EQ(decoded, binary_data); + + // Explicitly check decoding from hex and from base64. + { + std::string decoded_from_hex; + EXPECT_TRUE( + UserSecureManager::AsciiToBinary(kHexEncodedData, &decoded_from_hex)); + EXPECT_EQ(decoded_from_hex, binary_data); + } + { + std::string decoded_from_base64; + EXPECT_TRUE(UserSecureManager::AsciiToBinary(kBase64EncodedData, + &decoded_from_base64)); + EXPECT_EQ(decoded_from_base64, binary_data); + } + + // Test encoding and decoding empty strings. + std::string empty; + UserSecureManager::BinaryToAscii("", &empty); + EXPECT_EQ(empty, "#"); + EXPECT_TRUE(UserSecureManager::AsciiToBinary("#", &empty)); + EXPECT_EQ(empty, ""); + EXPECT_TRUE(UserSecureManager::AsciiToBinary("$", &empty)); + EXPECT_EQ(empty, ""); + + std::string u; // unused + + // Bad hex encodings. + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$11223", &u)); // odd size after header + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("ABCDEF01", &u)); // missing header + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A2GB34F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A:23A4F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A23A4$F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A2BG34F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A2:3A4F", &u)); // bad characters + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("$1A23A4F!", &u)); // bad characters + + // Bad base64 encodings. + EXPECT_FALSE(UserSecureManager::AsciiToBinary("#*", &u)); // invalid base64 + EXPECT_FALSE( + UserSecureManager::AsciiToBinary("#AAAA#AAAA", &u)); // bad characters +} + +} // namespace secure +} // namespace app +} // namespace firebase diff --git a/app/tests/semaphore_test.cc b/app/tests/semaphore_test.cc new file mode 100644 index 0000000000..a3262cc33f --- /dev/null +++ b/app/tests/semaphore_test.cc @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/semaphore.h" + +#include "app/src/thread.h" +#include "app/src/time.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +// Basic test of TryWait, to make sure that its successes and failures +// line up with what we'd expect, based on the initial count. +TEST(SemaphoreTest, TryWaitTests) { + firebase::Semaphore sem(2); + + // First time, should be able to get a value just fine. + EXPECT_EQ(sem.TryWait(), true); + + // Second time, should still be able to get a value. + EXPECT_EQ(sem.TryWait(), true); + + // Second time, we should be unable to acquire a lock. + EXPECT_EQ(sem.TryWait(), false); + + sem.Post(); + + // Should be able to get a lock now. + EXPECT_EQ(sem.TryWait(), true); +} + +// Test that semaphores work across threads. +// Blocks, after setting a thread to unlock itself in 1 second. +// If the thread doesn't unblock it, it will wait forever, triggering a test +// failure via timeout after 60 seconds, through the testing framework. +TEST(SemaphoreTest, MultithreadedTest) { + firebase::Semaphore sem(0); + + firebase::Thread( + [](void* data_) { + auto sem = static_cast(data_); + firebase::internal::Sleep(firebase::internal::kMillisecondsPerSecond); + sem->Post(); + }, + &sem) + .Detach(); + + // This will block, until the thread releases it. + sem.Wait(); +} + +// Tests that Timed Wait works as intended. +TEST(SemaphoreTest, TimedWait) { + firebase::Semaphore sem(0); + + int64_t start_ms = firebase::internal::GetTimestamp(); + EXPECT_FALSE(sem.TimedWait(firebase::internal::kMillisecondsPerSecond)); + int64_t finish_ms = firebase::internal::GetTimestamp(); + + assert(labs((finish_ms - start_ms) - + firebase::internal::kMillisecondsPerSecond) < + 0.10 * firebase::internal::kMillisecondsPerSecond); +} + +TEST(SemaphoreTest, DISABLED_MultithreadedStressTest) { + for (int i = 0; i < 10000; ++i) { + firebase::Semaphore sem(0); + + firebase::Thread thread = firebase::Thread( + [](void* data_) { + auto sem = static_cast(data_); + sem->Post(); + }, + &sem); + // This will block, until the thread releases it or it times out. + EXPECT_TRUE(sem.TimedWait(100)); + + thread.Join(); + } +} + +} // namespace diff --git a/app/tests/swizzle_test.mm b/app/tests/swizzle_test.mm new file mode 100644 index 0000000000..72f6b192c5 --- /dev/null +++ b/app/tests/swizzle_test.mm @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#import +#import +#include "app/src/util_ios.h" + +@interface SwizzlingTests : XCTestCase +@end + +@interface AppDelegate : UIResponder +@property(strong, nonatomic) NSMutableArray *selectorList; +@end + +@implementation AppDelegate + +- (instancetype)init { + if (self = [super init]) { + _selectorList = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + // Save the selectors and arguments that were called this way, to validate against later. + const char *selName = sel_getName([invocation selector]); + NSMutableString *toAdd = [NSMutableString stringWithUTF8String:selName]; + int numArgs = [[invocation methodSignature] numberOfArguments]; + for (int i = 2; i < numArgs; i++) { + __unsafe_unretained id arg; + [invocation getArgument:&arg atIndex:i]; + [toAdd appendString:[NSString stringWithFormat:@"|%p", arg]]; + } + [_selectorList addObject:toAdd]; +} + +@end + +@implementation SwizzlingTests + +- (void)testForwardInvocationPassThrough { + AppDelegate *appDelegate = [[AppDelegate alloc] init]; + + UIApplication *application = [UIApplication sharedApplication]; + NSURL *url = [[NSURL alloc] init]; + NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"myactivity"]; + void (^handler)(NSArray *); + NSData *data = [[NSData alloc] init]; + NSError *error = [[NSError alloc] init]; + firebase::util::UIBackgroundFetchResultFunction fetchHandler; + NSDictionary *dict = [[NSDictionary alloc] init]; + NSString *string = @"TestString"; + id testId = data; + + NSMutableArray *expectedList = [[NSMutableArray alloc] init]; + + // From invites_ios_startup.mm + [expectedList addObject:[NSString stringWithFormat:@"application:openURL:options:|%p|%p|%p", + application, url, dict]]; + [appDelegate application:application openURL:url options:dict]; + + [expectedList + addObject:[NSString stringWithFormat: + @"application:openURL:sourceApplication:annotation:|%p|%p|%p|%p", + application, url, string, testId]]; + [appDelegate application:application openURL:url sourceApplication:string annotation:testId]; + + [expectedList + addObject:[NSString stringWithFormat: + @"application:continueUserActivity:restorationHandler:|%p|%p|%p", + application, activity, handler]]; + [appDelegate application:application continueUserActivity:activity restorationHandler:handler]; + + [expectedList + addObject:[NSString stringWithFormat:@"applicationDidBecomeActive:|%p", application]]; + [appDelegate applicationDidBecomeActive:application]; + + // From instance_id.mm + [expectedList + addObject:[NSString + stringWithFormat: + @"application:didRegisterForRemoteNotificationsWithDeviceToken:|%p|%p", + application, data]]; + [appDelegate application:application didRegisterForRemoteNotificationsWithDeviceToken:data]; + + // From messaging.mm + [expectedList + addObject:[NSString stringWithFormat:@"application:didFinishLaunchingWithOptions:|%p|%p", + application, dict]]; + [appDelegate application:application didFinishLaunchingWithOptions:dict]; + + [expectedList + addObject:[NSString stringWithFormat:@"applicationDidEnterBackground:|%p", application]]; + [appDelegate applicationDidEnterBackground:application]; + + [expectedList + addObject:[NSString + stringWithFormat: + @"application:didFailToRegisterForRemoteNotificationsWithError:|%p|%p", + application, error]]; + [appDelegate application:application didFailToRegisterForRemoteNotificationsWithError:error]; + + [expectedList + addObject:[NSString stringWithFormat:@"application:didReceiveRemoteNotification:|%p|%p", + application, dict]]; + [appDelegate application:application didReceiveRemoteNotification:dict]; + + [expectedList addObject:[NSString stringWithFormat:@"application:didReceiveRemoteNotification:" + @"fetchCompletionHandler:|%p|%p|%p", + application, dict, fetchHandler]]; + [appDelegate application:application + didReceiveRemoteNotification:dict + fetchCompletionHandler:fetchHandler]; + + XCTAssertEqualObjects([appDelegate selectorList], expectedList); +} + +@end diff --git a/app/tests/thread_test.cc b/app/tests/thread_test.cc new file mode 100644 index 0000000000..9562d61388 --- /dev/null +++ b/app/tests/thread_test.cc @@ -0,0 +1,194 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/thread.h" + +#include "app/src/mutex.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +using ::testing::Eq; + +// Simple thread safe wrapper around a value T. +template +class ThreadSafe { + public: + explicit ThreadSafe(T value) : value_(value) {} + + T get() const { + firebase::MutexLock lock(const_cast(mtx_)); + return value_; + } + + void set(const T& value) { + firebase::MutexLock lock(mtx_); + value_ = value; + } + + private: + T value_; + firebase::Mutex mtx_; +}; + +TEST(ThreadTest, ThreadExecutesAndJoinWaitsForItToFinish) { + ThreadSafe value(false); + + firebase::Thread thread([](ThreadSafe* value) { value->set(true); }, + &value); + thread.Join(); + + ASSERT_THAT(value.get(), Eq(true)); +} + +TEST(ThreadTest, ThreadIsNotJoinableAfterJoin) { + firebase::Thread thread([] {}); + ASSERT_THAT(thread.Joinable(), Eq(true)); + + thread.Join(); + ASSERT_THAT(thread.Joinable(), Eq(false)); +} + +TEST(ThreadTest, ThreadIsNotJoinableAfterDetach) { + firebase::Thread thread([] {}); + ASSERT_THAT(thread.Joinable(), Eq(true)); + + thread.Detach(); + ASSERT_THAT(thread.Joinable(), Eq(false)); +} + +TEST(ThreadTest, ThreadShouldNotBeJoinableAfterBeingMoveAssignedOutOf) { + firebase::Thread source([] {}); + firebase::Thread target; + + ASSERT_THAT(source.Joinable(), Eq(true)); + + // cast due to lack of std::move in STLPort + target = static_cast(source); + ASSERT_THAT(source.Joinable(), Eq(false)); + ASSERT_THAT(target.Joinable(), Eq(true)); + target.Join(); +} + +TEST(ThreadTest, ThreadShouldNotBeJoinableAfterBeingMoveFrom) { + firebase::Thread source([] {}); + + ASSERT_THAT(source.Joinable(), Eq(true)); + + // cast due to lack of std::move in STLPort + firebase::Thread target(static_cast(source)); + ASSERT_THAT(source.Joinable(), Eq(false)); + ASSERT_THAT(target.Joinable(), Eq(true)); + target.Join(); +} + +TEST(ThreadDeathTest, MovingIntoRunningThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread = firebase::Thread(); + }, + ""); +} + +TEST(ThreadDeathTest, JoinEmptyThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread; + thread.Join(); + }, + ""); +} + +TEST(ThreadDeathTest, JoinThreadMultipleTimesShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Join(); + thread.Join(); + }, + ""); +} + +TEST(ThreadDeathTest, JoinDetachedThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Detach(); + thread.Join(); + }, + ""); +} + +TEST(ThreadDeathTest, DetachJoinedThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Join(); + thread.Detach(); + }, + ""); +} + +TEST(ThreadDeathTest, DetachEmptyThreadShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread; + + thread.Detach(); + }, + ""); +} + +TEST(ThreadDeathTest, DetachThreadMultipleTimesShouldAbort) { + ASSERT_DEATH( + { + firebase::Thread thread([] {}); + + thread.Detach(); + thread.Detach(); + }, + ""); +} + +TEST(ThreadDeathTest, WhenJoinableThreadIsDestructedShouldAbort) { + ASSERT_DEATH({ firebase::Thread thread([] {}); }, ""); +} + +TEST(ThreadTest, ThreadIsEqualToItself) { + firebase::Thread::Id thread_id = firebase::Thread::CurrentId(); + ASSERT_THAT(firebase::Thread::IsCurrentThread(thread_id), Eq(true)); +} + +TEST(ThreadTest, ThreadIsNotEqualToDifferentThread) { + ThreadSafe value(firebase::Thread::CurrentId()); + + firebase::Thread thread( + [](ThreadSafe* value) { + value->set(firebase::Thread::CurrentId()); + }, &value); + thread.Join(); + + ASSERT_THAT(firebase::Thread::IsCurrentThread(value.get()), Eq(false)); +} + +} // namespace diff --git a/app/tests/time_test.cc b/app/tests/time_test.cc new file mode 100644 index 0000000000..9c768cdf2d --- /dev/null +++ b/app/tests/time_test.cc @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Google LLC + * + * 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 "app/src/time.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace { + +#ifndef WIN32 +// Test that the normalize function works, for timespecs +TEST(TimeTests, NormalizeTest) { + timespec t; + t.tv_sec = 2; + t.tv_nsec = firebase::internal::kNanosecondsPerSecond * 5.5; + firebase::internal::NormalizeTimespec(&t); + + EXPECT_EQ(t.tv_sec, 7); + EXPECT_EQ(t.tv_nsec, firebase::internal::kNanosecondsPerSecond * 0.5); +} + +// Test the various conversions to and from timespecs. +TEST(TimeTests, ConversionTests) { + timespec t; + + // Test that we can convert timespecs into milliseconds. + t.tv_sec = 2; + t.tv_nsec = firebase::internal::kNanosecondsPerSecond * 0.5; + EXPECT_EQ(firebase::internal::TimespecToMs(t), 2500); + + // Test conversion of milliseconds into timespecs. + t = firebase::internal::MsToTimespec(6789); + EXPECT_EQ(t.tv_sec, 6); + EXPECT_EQ(t.tv_nsec, 789 * firebase::internal::kNanosecondsPerMillisecond); +} + +// Test the timespec compare function. +TEST(TimeTests, ComparisonTests) { + timespec t1, t2; + clock_gettime(CLOCK_REALTIME, &t1); + firebase::internal::Sleep(500); + clock_gettime(CLOCK_REALTIME, &t2); + + EXPECT_EQ(firebase::internal::TimespecCmp(t1, t2), -1); + EXPECT_EQ(firebase::internal::TimespecCmp(t2, t1), 1); + EXPECT_EQ(firebase::internal::TimespecCmp(t1, t1), 0); + EXPECT_EQ(firebase::internal::TimespecCmp(t2, t2), 0); +} +#endif + +// Test GetTimestamp function +TEST(TimeTests, GetTimestampTest) { + uint64_t start = firebase::internal::GetTimestamp(); + + firebase::internal::Sleep(500); + + uint64_t end = firebase::internal::GetTimestamp(); + + int64_t error = llabs(static_cast(end - start) - 500); + + EXPECT_TRUE(error < 0.10 * firebase::internal::kMillisecondsPerSecond); +} + +// Test GetTimestampEpoch function +TEST(TimeTests, GetTimestampEpochTest) { + uint64_t start = firebase::internal::GetTimestampEpoch(); + + firebase::internal::Sleep(500); + + uint64_t end = firebase::internal::GetTimestampEpoch(); + + int64_t error = llabs(static_cast(end - start) - 500); + + // Print out the epoch time so that we can verify the timestamp from the log + // This is the easiest way to verify if the function works in all platform +#ifdef __linux__ + printf("%lu -> %lu (%ld)\n", start, end, error); +#else + printf("%llu -> %llu (%lld)\n", start, end, error); +#endif // __linux__ + + EXPECT_TRUE(error < 0.10 * firebase::internal::kMillisecondsPerSecond); +} + +} // namespace diff --git a/app/tests/util_android_test.cc b/app/tests/util_android_test.cc new file mode 100644 index 0000000000..eeec59fa24 --- /dev/null +++ b/app/tests/util_android_test.cc @@ -0,0 +1,479 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/util_android.h" + +#include +#include + +#include "app/src/include/firebase/variant.h" +#include "app/src/semaphore.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/run_all_tests.h" + +namespace firebase { +namespace util { + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; +using ::testing::Ne; + +TEST(UtilAndroidTest, TestInitializeAndTerminate) { + // Initialize firebase util, including caching class/methods and dealing with + // embedded jar. + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + EXPECT_NE(nullptr, env); + jobject activity_object = firebase::testing::cppsdk::GetTestActivity(); + EXPECT_NE(nullptr, activity_object); + EXPECT_TRUE(Initialize(env, activity_object)); + + Terminate(env); +} + +TEST(JniUtilities, LocalToGlobalReference) { + JNIEnv *env = firebase::testing::cppsdk::GetTestJniEnv(); + jobject local_java_string = env->NewStringUTF("a string"); + jobject global_java_string = LocalToGlobalReference(env, local_java_string); + EXPECT_NE(nullptr, global_java_string); + env->DeleteGlobalRef(global_java_string); + + EXPECT_EQ(nullptr, LocalToGlobalReference(env, nullptr)); +} + +// Test execution on the main and background Java threads. +class JavaThreadContextTest : public ::testing::Test { + protected: + class ThreadContext { + public: + explicit ThreadContext(JavaThreadContext *java_thread_context = nullptr) + : started_(1), + complete_(1), + block_store_(1), + canceled_(false), + cancel_store_called_(false), + java_thread_context_(java_thread_context) { + thread_id_ = pthread_self(); + started_.Wait(); + complete_.Wait(); + block_store_.Wait(); + } + + // Wait for the thread to start. + void WaitForStart() { started_.Wait(); } + + // Wait for the thread to complete. + void WaitForCompletion() { complete_.Wait(); } + + // Continue Store() execution (if it's blocked). + void Continue() { block_store_.Post(); } + + // Get the thread ID. + pthread_t thread_id() const { return thread_id_; } + + // Get whether the thread was canceled. + bool canceled() const { return canceled_; } + + // Get whether CancelStore was called. + bool cancel_store_called() const { return cancel_store_called_; } + + // Store the current thread ID and signal thread completion. + static void Store(void *data) { + static_cast(data)->Store(false); + } + + // Wait for Continue() to be called then store the current thread ID + // if the context wasn't cancelled and signal thread completion. + static void WaitAndStore(void *data) { + static_cast(data)->Store(true); + } + + // Cancel the store operation. + static void CancelStore(void *data) { + static_cast(data)->cancel_store_called_ = true; + } + + private: + // Store the current thread ID if the object wasn't canceled and + // signal thread completion. + void Store(bool wait) { + if (wait) { + if (java_thread_context_) { + // Release the execution lock so the thread can be canceled. + java_thread_context_->ReleaseExecuteCancelLock(); + } + // Signal that the thread has started. + started_.Post(); + // Wait for Continue(). + block_store_.Wait(); + if (java_thread_context_) { + // If this method returns false, the thread is canceled. + canceled_ = !java_thread_context_->AcquireExecuteCancelLock(); + } + } else { + started_.Post(); + } + if (!canceled_) thread_id_ = pthread_self(); + complete_.Post(); + } + + private: + // ID of the thread. + pthread_t thread_id_; + // Signalled when the thread starts. + Semaphore started_; + // Signalled when a thread is complete. + Semaphore complete_; + // Used to block execution of Store(). + Semaphore block_store_; + // Whether the Store() operation was canceled. + bool canceled_; + // Whether CancelStore() was called. + bool cancel_store_called_; + JavaThreadContext *java_thread_context_; + }; + + void SetUp() override { + env_ = firebase::testing::cppsdk::GetTestJniEnv(); + ASSERT_TRUE(env_ != nullptr); + activity_ = firebase::testing::cppsdk::GetTestActivity(); + ASSERT_TRUE(activity_ != nullptr); + ASSERT_TRUE(Initialize(env_, activity_)); + } + + void TearDown() override { + ASSERT_TRUE(env_ != nullptr); + Terminate(env_); + } + + JNIEnv *env_; + jobject activity_; +}; + +TEST_F(JavaThreadContextTest, RunOnMainThread) { + ThreadContext thread_context(nullptr); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnMainThread(env_, activity_, ThreadContext::Store, &thread_context); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Ne(main_thread_id)); +} + +TEST_F(JavaThreadContextTest, RunOnMainThreadAndCancel) { + JavaThreadContext java_thread_context(env_); + ThreadContext thread_context(&java_thread_context); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnMainThread(env_, activity_, ThreadContext::WaitAndStore, &thread_context, + ThreadContext::CancelStore, &java_thread_context); + thread_context.WaitForStart(); + java_thread_context.Cancel(); + thread_context.Continue(); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Eq(main_thread_id)); + EXPECT_TRUE(thread_context.canceled()); + EXPECT_TRUE(thread_context.cancel_store_called()); +} + +TEST_F(JavaThreadContextTest, RunOnBackgroundThread) { + ThreadContext thread_context(nullptr); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnBackgroundThread(env_, ThreadContext::Store, &thread_context); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Ne(main_thread_id)); +} + +TEST_F(JavaThreadContextTest, RunOnBackgroundThreadAndCancel) { + JavaThreadContext java_thread_context(env_); + ThreadContext thread_context(&java_thread_context); + pthread_t main_thread_id = thread_context.thread_id(); + RunOnBackgroundThread(env_, ThreadContext::WaitAndStore, &thread_context, + ThreadContext::CancelStore, &java_thread_context); + thread_context.WaitForStart(); + java_thread_context.Cancel(); + thread_context.Continue(); + thread_context.WaitForCompletion(); + EXPECT_THAT(thread_context.thread_id(), Eq(main_thread_id)); + EXPECT_TRUE(thread_context.canceled()); + EXPECT_TRUE(thread_context.cancel_store_called()); +} + +/***** JavaObjectToVariant test *****/ +class JavaObjectToVariantTest : public ::testing::Test { + protected: + void SetUp() override { + env_ = firebase::testing::cppsdk::GetTestJniEnv(); + ASSERT_TRUE(env_ != nullptr); + activity_ = firebase::testing::cppsdk::GetTestActivity(); + ASSERT_TRUE(activity_ != nullptr); + ASSERT_TRUE(Initialize(env_, activity_)); + } + + void TearDown() override { + ASSERT_TRUE(env_ != nullptr); + Terminate(env_); + } + + const int kTestValueInt = 0x01234567; + const int64 kTestValueLong = 0x1234567ABCD1234L; + const int16 kTestValueShort = 0x3456; + const char kTestValueByte = 0x12; + const bool kTestValueBool = true; + const char *const kTestValueString = "Hello, world!"; + const float kTestValueFloat = 0.15625f; + const double kTestValueDouble = 1048576.15625; + + JNIEnv *env_; + jobject activity_; +}; + +TEST_F(JavaObjectToVariantTest, TestFundamentalTypes) { + // null converts to Variant::kTypeNull. + { + jobject obj = nullptr; + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), Variant::Null()); + } + // Integral types convert to Variant::kTypeInt64. This includes Date. + { + // Integer + jobject obj = + env_->NewObject(integer_class::GetClass(), + integer_class::GetMethodId(integer_class::kConstructor), + static_cast(kTestValueInt)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueInt)) + << "Failed to convert Integer"; + env_->DeleteLocalRef(obj); + } + { + // Short + jobject obj = + env_->NewObject(short_class::GetClass(), + short_class::GetMethodId(short_class::kConstructor), + static_cast(kTestValueShort)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueShort)) + << "Failed to convert Short"; + env_->DeleteLocalRef(obj); + } + { + // Long + jobject obj = + env_->NewObject(long_class::GetClass(), + long_class::GetMethodId(long_class::kConstructor), + static_cast(kTestValueLong)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueLong)) + << "Failed to convert Long"; + env_->DeleteLocalRef(obj); + } + { + // Byte + jobject obj = + env_->NewObject(byte_class::GetClass(), + byte_class::GetMethodId(byte_class::kConstructor), + static_cast(kTestValueByte)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueByte)) + << "Failed to convert Byte"; + env_->DeleteLocalRef(obj); + } + { + // Date becomes an Int64 of milliseconds since epoch, which is also what the + // Java Date constructor happens to take as an argument. + jobject obj = env_->NewObject(date::GetClass(), + date::GetMethodId(date::kConstructorWithTime), + static_cast(kTestValueLong)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromInt64(kTestValueLong)) + << "Failed to convert Date"; + env_->DeleteLocalRef(obj); + } + + // Floating point types convert to Variant::kTypeDouble. + { + // Float + jobject obj = + env_->NewObject(float_class::GetClass(), + float_class::GetMethodId(float_class::kConstructor), + static_cast(kTestValueFloat)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromDouble(kTestValueFloat)) + << "Failed to convert Float"; + env_->DeleteLocalRef(obj); + } + { + // Double + jobject obj = + env_->NewObject(double_class::GetClass(), + double_class::GetMethodId(double_class::kConstructor), + static_cast(kTestValueDouble)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromDouble(kTestValueDouble)) + << "Failed to convert Double"; + env_->DeleteLocalRef(obj); + } + // Boolean converts to Variant::kTypeBool. + { + jobject obj = + env_->NewObject(boolean_class::GetClass(), + boolean_class::GetMethodId(boolean_class::kConstructor), + static_cast(kTestValueBool)); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromBool(kTestValueBool)) + << "Failed to convert Boolean"; + env_->DeleteLocalRef(obj); + } + // String converts to Variant::kTypeMutableString. + { + jobject obj = env_->NewStringUTF(kTestValueString); + EXPECT_EQ(util::JavaObjectToVariant(env_, obj), + Variant::FromMutableString(kTestValueString)) + << "Failed to convert String"; + env_->DeleteLocalRef(obj); + } +} + +TEST_F(JavaObjectToVariantTest, TestContainerTypes) { + // Array and List types convert to Variant::kTypeVector. + { + // Two tests in one: Array of Objects, and ArrayList. + // Both contain {Integer, Float, String, Null}. + + jobjectArray array = env_->NewObjectArray(4, object::GetClass(), nullptr); + jobject container = + env_->NewObject(array_list::GetClass(), + array_list::GetMethodId(array_list::kConstructor)); + { + // Element 1: Integer + jobject obj = env_->NewObject( + integer_class::GetClass(), + integer_class::GetMethodId(integer_class::kConstructor), + static_cast(kTestValueInt)); + env_->SetObjectArrayElement(array, 0, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + env_->DeleteLocalRef(obj); + } + { + // Element 2: Float + jobject obj = + env_->NewObject(float_class::GetClass(), + float_class::GetMethodId(float_class::kConstructor), + static_cast(kTestValueFloat)); + env_->SetObjectArrayElement(array, 1, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + env_->DeleteLocalRef(obj); + } + { + // Element 3: String + jobject obj = env_->NewStringUTF(kTestValueString); + env_->SetObjectArrayElement(array, 2, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + env_->DeleteLocalRef(obj); + } + { + // Element 4: Null + jobject obj = nullptr; + env_->SetObjectArrayElement(array, 3, obj); + env_->CallBooleanMethod(container, + array_list::GetMethodId(array_list::kAdd), obj); + } + + Variant expected = Variant::EmptyVector(); + expected.vector().push_back(Variant::FromInt64(kTestValueInt)); + expected.vector().push_back(Variant::FromDouble(kTestValueFloat)); + expected.vector().push_back(Variant::FromMutableString(kTestValueString)); + expected.vector().push_back(Variant::Null()); + + EXPECT_EQ(util::JavaObjectToVariant(env_, array), expected) + << "Failed to convert Array of Object{Integer, Float, String, Null}"; + EXPECT_EQ(util::JavaObjectToVariant(env_, container), expected) + << "Failed to convert ArrayList{Integer, Float, String, Null}"; + env_->DeleteLocalRef(array); + env_->DeleteLocalRef(container); + } + // Map type converts to Variant::kTypeMap. + { + // Test a HashMap of String to {Integer, Float, String, Null} + // Only test keys that are strings, as that's all Java provides. + jobject container = env_->NewObject( + hash_map::GetClass(), hash_map::GetMethodId(hash_map::kConstructor)); + { + // Element 1: Integer + jobject key = env_->NewStringUTF("one"); + jobject obj = env_->NewObject( + integer_class::GetClass(), + integer_class::GetMethodId(integer_class::kConstructor), + static_cast(kTestValueInt)); + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + env_->DeleteLocalRef(obj); + } + { + // Element 2: Float + jobject key = env_->NewStringUTF("two"); + jobject obj = + env_->NewObject(float_class::GetClass(), + float_class::GetMethodId(float_class::kConstructor), + static_cast(kTestValueFloat)); + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + env_->DeleteLocalRef(obj); + } + { + // Element 3: String + jobject key = env_->NewStringUTF("three"); + jobject obj = env_->NewStringUTF(kTestValueString); + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + env_->DeleteLocalRef(obj); + } + { + // Element 4: Null + jobject key = env_->NewStringUTF("four"); + jobject obj = nullptr; + jobject discard = env_->CallObjectMethod( + container, map::GetMethodId(map::kPut), key, obj); + env_->DeleteLocalRef(discard); + env_->DeleteLocalRef(key); + } + + Variant expected = Variant::EmptyMap(); + expected.map()[Variant("one")] = Variant::FromInt64(kTestValueInt); + expected.map()[Variant("two")] = Variant::FromDouble(kTestValueFloat); + expected.map()[Variant("three")] = + Variant::FromMutableString(kTestValueString); + expected.map()[Variant("four")] = Variant::Null(); + + EXPECT_EQ(util::JavaObjectToVariant(env_, container), expected) + << "Failed to convert Map of String to {Integer, Float, String, Null}"; + env_->DeleteLocalRef(container); + } + // TODO(b/113619056): Test complex containers containing other containers. +} + +// TODO(b/113619056): Tests for VariantToJavaObject. + +} // namespace util +} // namespace firebase diff --git a/app/tests/util_ios_test.mm b/app/tests/util_ios_test.mm new file mode 100644 index 0000000000..b9c231f8bc --- /dev/null +++ b/app/tests/util_ios_test.mm @@ -0,0 +1,650 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +#import + +#include "app/src/include/firebase/variant.h" +#include "app/src/util_ios.h" + +typedef firebase::util::ObjCPointer NSStringCpp; +OBJ_C_PTR_WRAPPER_NAMED(NSStringHandle, NSString); +OBJ_C_PTR_WRAPPER(NSString); + +@interface ObjCPointerTests : XCTestCase +@end + +@implementation ObjCPointerTests + +- (void)testConstructAndGet { + NSStringCpp cpp; + NSStringHandle handle; + NSStringPointer pointer; + XCTAssertEqual(cpp.get(), nil); + XCTAssertEqual(handle.get(), nil); + XCTAssertEqual(pointer.get(), nil); +} + +- (void)testConstructWithObjectAndGet { + NSString* nsstring = @"hello"; + NSStringCpp cpp(nsstring); + NSStringHandle handle(nsstring); + NSStringPointer pointer(nsstring); + NSStringHandle from_base_type(cpp); + XCTAssertEqual(cpp.get(), nsstring); + XCTAssertEqual(handle.get(), nsstring); + XCTAssertEqual(pointer.get(), nsstring); + XCTAssertEqual(from_base_type.get(), nsstring); +} + +- (void)testRelease { + NSString *nsstring = @"hello"; + NSStringCpp cpp(nsstring); + XCTAssertEqual(cpp.get(), nsstring); + cpp.release(); + XCTAssertEqual(cpp.get(), nil); +} + +- (void)testBoolOperator { + NSStringCpp cpp(@"hello"); + XCTAssertTrue(cpp); + cpp.release(); + XCTAssertFalse(cpp); +} + +- (void)testReset { + NSString* hello = @"hello"; + NSString* goodbye = @"goodbye"; + NSStringCpp cpp(hello); + XCTAssertEqual(cpp.get(), hello); + cpp.reset(goodbye); + XCTAssertEqual(cpp.get(), goodbye); +} + +- (void)testAssign { + NSString* hello = @"hello"; + NSString* goodbye = @"goodbye"; + NSStringCpp cpp(hello); + NSStringHandle handle(hello); + NSStringPointer pointer(hello); + XCTAssertEqual(cpp.get(), hello); + XCTAssertEqual(*cpp, hello); + XCTAssertEqual(handle.get(), hello); + XCTAssertEqual(*handle, hello); + XCTAssertEqual(pointer.get(), hello); + XCTAssertEqual(*pointer, hello); + XCTAssertEqual((*cpp).length, 5); + XCTAssertEqual((*handle).length, 5); + XCTAssertEqual((*pointer).length, 5); + cpp = goodbye; + handle = goodbye; + pointer = goodbye; + XCTAssertEqual(cpp.get(), goodbye); + XCTAssertEqual(handle.get(), goodbye); + XCTAssertEqual(pointer.get(), goodbye); +} + +- (void)testSafeGet { + NSString* hello = @"hello"; + NSStringCpp cpp(hello); + NSStringHandle handle(hello); + NSStringPointer pointer(hello); + XCTAssertEqual(NSStringCpp::SafeGet(&cpp), hello); + XCTAssertEqual(NSStringHandle::SafeGet(&handle), hello); + XCTAssertEqual(NSStringPointer::SafeGet(&pointer), hello); + cpp.release(); + handle.release(); + pointer.release(); + XCTAssertEqual(NSStringCpp::SafeGet(&cpp), nil); + XCTAssertEqual(NSStringHandle::SafeGet(&handle), nil); + XCTAssertEqual(NSStringPointer::SafeGet(&pointer), nil); + XCTAssertEqual(NSStringCpp::SafeGet(nullptr), nil); +} + +@end + +using ::firebase::Variant; +using ::firebase::util::IdToVariant; +using ::firebase::util::VariantToId; + +@interface IdToVariantTests : XCTestCase +@end + +@implementation IdToVariantTests + +- (void)testNil { + // Check that nil maps to a null variant and that a non-nil value does not map + // to a null variant. + { + // Nil id. + id value = nil; + Variant variant = IdToVariant(value); + XCTAssertTrue(variant.is_null()); + } + { + // Non-nil id. + id value = [NSNumber numberWithInteger:0]; + Variant variant = IdToVariant(value); + XCTAssertFalse(variant.is_null()); + } +} + +- (void)testInteger { + // Check that integers map to the correct variant, even when those numbers + // exceed the maximum integer value. + { + // Check that the integer 0 maps to an integer variant holding 0. + id number = [NSNumber numberWithInteger:0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 0); + } + { + // Check that the integer 1 maps to an integer variant holding 1. + id number = [NSNumber numberWithInteger:1]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 1); + } + { + // Check that the integer 10 maps to an integer variant holding 10. + id number = [NSNumber numberWithInteger:10]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 10); + } + { + // Check that a variant can hander an integer larger than the largest 32 bit + // int. + id number = [NSNumber numberWithInteger:5000000000ll]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_int64()); + XCTAssertTrue(variant.int64_value() == 5000000000ll); + } +} + +- (void)testDouble { + // Check that integers map to the correct variant, even when those numbers + // exceed the maximum 64 bit integer value. + { + // Check that the double 0.0 maps to a double variant holding 0.0. + id number = [NSNumber numberWithDouble:0.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 0.0); + } + { + // Check that a variant can hander fractional values. + id number = [NSNumber numberWithDouble:0.5]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 0.5); + } + { + // Check that the double 1.0 maps to a double variant holding 1.0. + id number = [NSNumber numberWithDouble:1.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 1.0); + } + { + // Check that the double 10.0 maps to a double variant holding 10.0. + id number = [NSNumber numberWithDouble:10.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 10.0); + } + { + // Check that a variant can hander a double larger than the largest 32 bit + // int. + id number = [NSNumber numberWithDouble:5000000000.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 5000000000.0); + } + { + // Check that a variant can hander a double larger than the largest 64 bit + // int. + id number = [NSNumber numberWithDouble:20000000000000000000.0]; + Variant variant = IdToVariant(number); + XCTAssertTrue(variant.is_double()); + XCTAssertTrue(variant.double_value() == 20000000000000000000.0); + } +} + +- (void)testBool { + // Check that boolean values map to the correct boolean variant. + { + id value = [NSNumber numberWithBool:YES]; + Variant variant = IdToVariant(value); + XCTAssertTrue(variant.is_bool()); + XCTAssertTrue(variant.bool_value() == true); + } + { + id value = [NSNumber numberWithBool:NO]; + Variant variant = IdToVariant(value); + XCTAssertTrue(variant.is_bool()); + XCTAssertTrue(variant.bool_value() == false); + } +} + +- (void)testString { + // Check that NSStrings map to the correct std::string variants. + { + // Empty string. + id str = @""; + Variant variant = IdToVariant(str); + XCTAssertTrue(variant.is_string()); + XCTAssertTrue(variant.is_mutable_string()); + XCTAssertTrue(variant.string_value() == std::string("")); + } + { + // Non-empty string. + id str = @"Test With Very Very Long String"; + Variant variant = IdToVariant(str); + XCTAssertTrue(variant.is_string()); + XCTAssertTrue(variant.is_mutable_string()); + XCTAssertTrue(variant.string_value() == std::string("Test With Very Very Long String")); + } +} + +- (void)testVector { + // Check that NSArrays map to the correct vector variants. + { + // Empty NSArray to empty vector. + id array = @[]; + std::vector expected; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray of numbers to vector of integer variants. + id array = @[ @1, @2, @3, @4, @5 ]; + std::vector expected{1, 2, 3, 4, 5}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray of NSStrings to vector of std::string variants. + id array = @[ @"This", @"is", @"a", @"test." ]; + std::vector expected{"This", "is", "a", "test."}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray of various types to vector of variants holding varying types. + id array = @[ @"Different types", @10, @3.14 ]; + std::vector expected{std::string("Different types"), 10, 3.14}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } + { + // NSArray containing an NSArray and an NSDictionary to an std::vector + // holding an std::vector and std::map + id array = @[ @[ @1, @2, @3 ], @{ @4 : @5, @6 : @7, @8 : @9 } ]; + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::vector expected{Variant(vector_element), + Variant(map_element)}; + Variant variant = IdToVariant(array); + XCTAssertTrue(variant.is_vector()); + XCTAssertTrue(variant.vector() == expected); + } +} + +- (void)testMap { + { + // Check that an empty NSDictionary maps to an empty std::map. + id dictionary = @{}; + std::map expected; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } + { + // Check that a NSDictionary of strings to numbers maps to a std::map of + // string variants to number variants. + id dictionary = @{ + @"test1" : @1, + @"test2" : @2, + @"test3" : @3, + @"test4" : @4, + @"test5" : @5 + }; + std::map expected{ + std::make_pair(Variant("test1"), Variant(1)), + std::make_pair(Variant("test2"), Variant(2)), + std::make_pair(Variant("test3"), Variant(3)), + std::make_pair(Variant("test4"), Variant(4)), + std::make_pair(Variant("test5"), Variant(5))}; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } + { + // Check that a NSDictionary of various types maps to a std::map of variants + // holding various types. + id dictionary = @{ @20 : @"Different types", @6.28 : @10, @"Blah" : @3.14 }; + std::map expected{ + std::make_pair(Variant(20), Variant("Different types")), + std::make_pair(Variant(6.28), Variant(10)), + std::make_pair(Variant("Blah"), Variant(3.14))}; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } + { + // Check that a NSDictionary of NSArray-to-NSDictionary maps to an std::map + // of vector-to-map + id dictionary = @{ @[ @1, @2, @3 ] : @{@4 : @5, @6 : @7, @8 : @9} }; + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::map expected{ + std::make_pair(Variant(vector_element), Variant(map_element))}; + Variant variant = IdToVariant(dictionary); + XCTAssertTrue(variant.is_map()); + XCTAssertTrue(variant.map() == expected); + } +} + +@end + +@interface VariantToIdTests : XCTestCase +@end + +@implementation VariantToIdTests + +- (void)testNil { + // Check that null variant maps to nil variant and that a non-null does not + // map to a nil id. + { + // Null variant. + Variant variant; + id value = VariantToId(variant); + XCTAssertTrue(value == [NSNull null]); + } + { + // Non-null variant. + Variant variant(10); + id value = VariantToId(variant); + XCTAssertTrue(value != [NSNull null]); + } +} + +- (void)testInteger { + // Check that integers map to the correct variant, even when those numbers + // exceed the maximum integer value. + { + // Check that the variant 0 maps to an NSNumber holding 0. + Variant variant(0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 0); + } + { + // Check that the variant 1 maps to an NSNumber holding 1. + Variant variant(1); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 1); + } + { + // Check that the variant 10 maps to an NSNumber holding 10. + Variant variant(10); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 10); + } + { + // Check that a variant can hander an integer larger than the largest 32 bit + // int. + Variant variant(5000000000ll); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number longLongValue] == 5000000000ll); + } +} + +- (void)testDouble { + // Check that doubles map to the correct variant, even when those numbers + // exceed the maximum integer value. + { + // Check that the variant 0.0 maps to an NSNumber holding 0.0. + Variant variant(0.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 0.0); + } + { + // Check that a variant can hander fractional values. + Variant variant(0.5); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 0.5); + } + { + // Check that the variant 1.0 maps to an NSNumber holding 1.0. + Variant variant(1.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 1.0); + } + { + // Check that the variant 10.0 maps to an NSNumber holding 10.0. + Variant variant(10.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 10.0); + } + { + // Check that a variant can hander a double larger than the largest 32 bit + // int. + Variant variant(5000000000.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 5000000000.0); + } + { + // Check that a variant can hander a double larger than the largest 64 bit + // int. + Variant variant(20000000000000000000.0); + id number = VariantToId(variant); + XCTAssertTrue([number isKindOfClass:[NSNumber class]]); + XCTAssertTrue([number doubleValue] == 20000000000000000000.0); + } +} + +- (void)testBool { + // Check that boolean variants map to the correct NSNumbers. + { + Variant variant(true); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSNumber class]]); + XCTAssertTrue([value boolValue] == YES); + } + { + Variant variant(false); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSNumber class]]); + XCTAssertTrue([value boolValue] == NO); + } +} + +- (void)testString { + { + // Empty static string. + const char* input_string = ""; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@""]); + } + { + // Empty mutable string. + std::string input_string = ""; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@""]); + } + { + // Non-empty static string. + const char* input_string = "Test"; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@"Test"]); + } + { + // Non-empty mutable string. + std::string input_string = "Test"; + Variant variant(input_string); + id value = VariantToId(variant); + XCTAssertTrue([value isKindOfClass:[NSString class]]); + XCTAssertTrue([value isEqualToString:@"Test"]); + } +} + +- (void)testVector { + // Check that std::vectors map to NSArrays, even when those numbers + // exceed the maximum integer value. + { + // Empty std::vector to empty NSArray. + std::vector vector; + id expected = @[]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector of integers to NSArray of NSNumbers. + std::vector vector{1, 2, 3, 4, 5}; + id expected = @[ @1, @2, @3, @4, @5 ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector of static and mutable strings to NSArray of NSStrings. + std::vector vector{"This", std::string("is"), "a", + std::string("test.")}; + id expected = @[ @"This", @"is", @"a", @"test." ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector of various types to NSArray of various types. + std::vector vector{"Different types", 10, 3.14}; + id expected = @[ @"Different types", @10, @3.14 ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } + { + // std::vector containing a vector and map to an NSArray containing an + // NSArray and an NSDictionary. + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::vector vector{Variant(vector_element), Variant(map_element)}; + id expected = @[ @[ @1, @2, @3 ], @{ @4 : @5, @6 : @7, @8 : @9 } ]; + Variant variant(vector); + id array = VariantToId(variant); + XCTAssertTrue([array isKindOfClass:[NSArray class]]); + XCTAssertTrue([array isEqual:expected]); + } +} + +- (void)testMap { + // Check that an std::maps map to NSDictionarys with correct types. + { + // Check that empty std::map maps to an empty NSDictionary. + std::map map; + id expected = @{}; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } + { + // Check that an std::map of strings to numbers maps to an NSDictionary of + // NSString to NSNumbers. + std::map map{ + std::make_pair(Variant("test1"), Variant(1)), + std::make_pair(Variant("test2"), Variant(2)), + std::make_pair(Variant("test3"), Variant(3)), + std::make_pair(Variant("test4"), Variant(4)), + std::make_pair(Variant("test5"), Variant(5))}; + id expected = @{ + @"test1" : @1, + @"test2" : @2, + @"test3" : @3, + @"test4" : @4, + @"test5" : @5 + }; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } + { + // Check that an std::map of various types maps to an NSDictionary of + // various types. + std::map map{ + std::make_pair(Variant(20), Variant(std::string("Different types"))), + std::make_pair(Variant(6.28), Variant(10)), + std::make_pair(Variant("Blah"), Variant(3.14))}; + id expected = @{ @20 : @"Different types", @6.28 : @10, @"Blah" : @3.14 }; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } + { + // Check that an std::map of vector-to-map maps to a NSDictionary of + // NSArray-to-NSDictionary + std::vector vector_element{1, 2, 3}; + std::map map_element{ + std::make_pair(Variant(4), Variant(5)), + std::make_pair(Variant(6), Variant(7)), + std::make_pair(Variant(8), Variant(9))}; + std::map map{ + std::make_pair(Variant(vector_element), Variant(map_element))}; + id expected = @{ @[ @1, @2, @3 ] : @{@4 : @5, @6 : @7, @8 : @9} }; + Variant variant(map); + id dictionary = VariantToId(variant); + XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([dictionary isEqual:expected]); + } +} + +@end diff --git a/app/tests/uuid_test.cc b/app/tests/uuid_test.cc new file mode 100644 index 0000000000..fffe9f8568 --- /dev/null +++ b/app/tests/uuid_test.cc @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google LLC + * + * 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 "app/src/uuid.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Contains; +using ::testing::Ne; + +// Generate a UUID and make sure it's not zero. +TEST(UuidTest, Generate) { + firebase::internal::Uuid uuid; + memset(&uuid, 0, sizeof(uuid)); + uuid.Generate(); + EXPECT_THAT(uuid.data, Contains(Ne(0))); +} + +// Generate two UUIDs and verify they're different. +TEST(UuidTest, GenerateDifferent) { + firebase::internal::Uuid uuid[2]; + memset(&uuid, 0, sizeof(uuid)); + uuid[0].Generate(); + uuid[1].Generate(); + EXPECT_THAT(memcmp(uuid[0].data, uuid[1].data, sizeof(uuid[0].data)), Ne(0)); +} diff --git a/app/tests/variant_test.cc b/app/tests/variant_test.cc new file mode 100644 index 0000000000..cde874e459 --- /dev/null +++ b/app/tests/variant_test.cc @@ -0,0 +1,1186 @@ +/* + * Copyright 2016 Google LLC + * + * 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 "app/src/include/firebase/variant.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Eq; +using ::testing::Gt; +using ::testing::IsEmpty; +using ::testing::Lt; +using ::testing::Ne; +using ::testing::Not; +using ::testing::Pair; +using ::testing::Property; +using ::testing::ResultOf; +using ::testing::StrEq; +using ::testing::UnorderedElementsAre; + +namespace firebase { +namespace internal { +class VariantInternal { + public: + static constexpr uint32_t kInternalTypeSmallString = + Variant::kInternalTypeSmallString; + + static uint32_t type(const Variant& v) { + return v.type_; + } +}; +} // namespace internal +} // namespace firebase + +using firebase::internal::VariantInternal; + +namespace firebase { +namespace testing { + +const int64_t kTestInt64 = 12345L; +const char* kTestString = "Hello, world!"; +const std::string kTestSmallString = " kTestVector = {int64_t(1L), "one", true, 1.0}; +// NOLINTNEXTLINE +const std::vector kTestComplexVector = {int64_t(2L), "two", + kTestVector, false, 2.0}; +const uint8_t kTestBlobData[] = {89, 0, 65, 198, 4, 99, 0, 9}; +const size_t kTestBlobSize = sizeof(kTestBlobData); // size in bytes +std::map g_test_map; // NOLINT +std::map g_test_complex_map; // NOLINT + +class VariantTest : public ::testing::Test { + protected: + VariantTest() {} + void SetUp() override { + g_test_map.clear(); + g_test_map["first"] = 101; + g_test_map["second"] = 202.2; + g_test_map["third"] = "three"; + + g_test_complex_map.clear(); + g_test_complex_map["one"] = kTestString; + g_test_complex_map[2] = 123; + g_test_complex_map[3.0] = + Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + g_test_complex_map[kTestVector] = kTestComplexVector; + g_test_complex_map[std::string("five")] = g_test_map; + g_test_complex_map[Variant::FromMutableBlob(kTestBlobData, kTestBlobSize)] = + kTestMutableString; + } +}; + +TEST_F(VariantTest, TestScalarTypes) { + { + Variant v; + EXPECT_THAT(v.type(), Eq(Variant::kTypeNull)); + EXPECT_TRUE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestInt64); + EXPECT_THAT(v.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v.int64_value(), Eq(kTestInt64)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + // Ensure that 0 comes through as an integer, not a bool. + Variant v(0); + EXPECT_THAT(v.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v.int64_value(), Eq(0)); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestString); + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v.string_value(), Eq(kTestString)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestSmallString); + EXPECT_THAT(VariantInternal::type(v), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v.string_value(), Eq(kTestSmallString)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + + // Should be able to upgrade to mutable string + EXPECT_THAT(v.mutable_string(), Eq(kTestSmallString)); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableString)); + } + { + Variant v(kTestMutableString); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v.mutable_string(), Eq(kTestMutableString)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestBool); + EXPECT_THAT(v.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v.bool_value(), Eq(kTestBool)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } + { + Variant v(kTestDouble); + EXPECT_THAT(v.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v.double_value(), Eq(kTestDouble)); + EXPECT_FALSE(v.is_null()); + EXPECT_TRUE(v.is_fundamental_type()); + EXPECT_FALSE(v.is_container_type()); + } +} + +TEST_F(VariantTest, TestInvalidTypeAsserts1) { + { + Variant v; + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestInt64); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestDouble); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestBool); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } +} + +TEST_F(VariantTest, TestInvalidTypeAsserts2) { + { + Variant v(kTestString); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestMutableString); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } + { + Variant v(kTestVector); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.map(), DEATHTEST_SIGABRT); + } + { + Variant v(g_test_map); + EXPECT_DEATH(v.int64_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.double_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.bool_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.string_value(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.mutable_string(), DEATHTEST_SIGABRT); + EXPECT_DEATH(v.vector(), DEATHTEST_SIGABRT); + } +} + +TEST_F(VariantTest, TestMutableStringPromotion) { + Variant v("Hello!"); + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v.string_value(), StrEq("Hello!")); + (void)v.mutable_string(); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v.mutable_string(), StrEq("Hello!")); + EXPECT_THAT(v.string_value(), StrEq("Hello!")); + v.mutable_string()[5] = '?'; + EXPECT_THAT(v.mutable_string(), StrEq("Hello?")); + EXPECT_THAT(v.string_value(), StrEq("Hello?")); + v.set_string_value("Goodbye."); + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v.string_value(), StrEq("Goodbye.")); +} + +TEST_F(VariantTest, TestSmallString) { + std::string max_small_str; + + if (sizeof(void*) == 8) { + max_small_str = "1234567812345678"; // 16 bytes on x64 + } else { + max_small_str = "12345678"; // 8 bytes on x32 + } + + std::string small_str = max_small_str; + small_str.pop_back(); // Make room for the trailing \0. + + // Test construction from std::string + Variant v1(small_str); + EXPECT_THAT(VariantInternal::type(v1), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v1.string_value(), StrEq(small_str.c_str())); + + // Test copy constructor + Variant v1c(v1); + EXPECT_THAT(VariantInternal::type(v1c), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v1c.string_value(), StrEq(small_str.c_str())); + +#ifdef FIREBASE_USE_MOVE_OPERATORS + // Test move constructor + Variant temp(small_str); + Variant v2(std::move(temp)); + EXPECT_THAT(VariantInternal::type(v2), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v2.string_value(), StrEq(small_str.c_str())); +#endif + + // Test construction of string bigger than max + Variant v3(max_small_str); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v3.string_value(), StrEq(max_small_str.c_str())); + + // Copy normal string to ensure type changes to mutable string + v1 = v3; + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1.string_value(), StrEq(max_small_str.c_str())); + + // Test set using smaller string + v1c.set_mutable_string("a"); + EXPECT_THAT(VariantInternal::type(v1c), + Eq(VariantInternal::kInternalTypeSmallString)); + EXPECT_THAT(v1c.string_value(), StrEq("a")); + + // Test can set small string as mutable + v1c.set_mutable_string("b", false); + EXPECT_THAT(v1c.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1c.string_value(), StrEq("b")); +} + +TEST_F(VariantTest, TestBasicVector) { + Variant v1(kTestInt64); + Variant v2(kTestString); + Variant v3(kTestDouble); + Variant v4(kTestBool); + Variant v5(kTestMutableString); + Variant v(std::vector{v1, v2, v3, v4, v5}); + + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_TRUE(v.is_container_type()); + EXPECT_FALSE(v.is_fundamental_type()); + EXPECT_THAT( + v.vector(), + ElementsAre( + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(kTestInt64))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, Eq(kTestString))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(kTestDouble))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeBool)), + Property(&Variant::bool_value, Eq(kTestBool))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeMutableString)), + Property(&Variant::mutable_string, Eq(kTestMutableString))))); +} + +TEST_F(VariantTest, TestConstructingVectorViaTemplate) { + { + std::vector list{8, 6, 7, 5, 3, 0, 9}; + Variant v(list); + EXPECT_THAT( + v.vector(), + ElementsAre(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(8))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(6))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(7))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(5))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(3))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(0))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(9))))); + } + { + std::vector list{0, 1.1, 2.2, 3.3, 4}; + Variant v(list); + EXPECT_THAT( + v.vector(), + ElementsAre(AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(0))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(1.1))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(2.2))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(3.3))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(4))))); + } + { + std::vector list1 { + "hello", + "world", + "how", + "are", + "you with more chars" + }; + std::vector list2 { + "hello", + "world", + "how", + "are", + "you with more chars" + }; + Variant v1(list1), v2(list2); + EXPECT_THAT( + v1.vector(), + ElementsAre( + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("hello"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("world"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("how"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("are"))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, + StrEq("you with more chars"))))); + EXPECT_THAT( + v2.vector(), + ElementsAre( + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("hello"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("world"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("how"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(VariantInternal::kInternalTypeSmallString)), + Property(&Variant::string_value, StrEq("are"))), + AllOf(ResultOf(&VariantInternal::type, + Eq(Variant::kTypeMutableString)), + Property(&Variant::string_value, + StrEq("you with more chars"))))); + + // Static and mutable strings are considered equal. So these should be + // equal. + EXPECT_EQ(v1, v2); + } +} + +TEST_F(VariantTest, TestNestedVectors) { + Variant v(std::vector{ + kTestInt64, std::vector{10, 20, 30, 40, 50}, + std::vector{"apples", "oranges", "lemons"}, + std::vector{"sneezy", "bashful", "dopey", "doc"}, + std::vector{true, false, false, true, false}, kTestString, + std::vector{3.14159, 2.71828, 1.41421, 0}, kTestBool, + std::vector{int64_t(100L), "one hundred", 100.0, + std::vector{}, Variant(), 0}, + kTestDouble}); + + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT( + v.vector(), + ElementsAre( + Property(&Variant::int64_value, Eq(kTestInt64)), + Property(&Variant::vector, + ElementsAre(Property(&Variant::int64_value, Eq(10)), + Property(&Variant::int64_value, Eq(20)), + Property(&Variant::int64_value, Eq(30)), + Property(&Variant::int64_value, Eq(40)), + Property(&Variant::int64_value, Eq(50)))), + Property( + &Variant::vector, + ElementsAre(Property(&Variant::string_value, StrEq("apples")), + Property(&Variant::string_value, StrEq("oranges")), + Property(&Variant::string_value, StrEq("lemons")))), + Property( + &Variant::vector, + ElementsAre(Property(&Variant::string_value, StrEq("sneezy")), + Property(&Variant::string_value, StrEq("bashful")), + Property(&Variant::string_value, StrEq("dopey")), + Property(&Variant::string_value, StrEq("doc")))), + Property(&Variant::vector, + ElementsAre(Property(&Variant::bool_value, Eq(true)), + Property(&Variant::bool_value, Eq(false)), + Property(&Variant::bool_value, Eq(false)), + Property(&Variant::bool_value, Eq(true)), + Property(&Variant::bool_value, Eq(false)))), + Property(&Variant::string_value, Eq(kTestString)), + Property(&Variant::vector, + ElementsAre(Property(&Variant::double_value, Eq(3.14159)), + Property(&Variant::double_value, Eq(2.71828)), + Property(&Variant::double_value, Eq(1.41421)), + Property(&Variant::double_value, Eq(0)))), + Property(&Variant::bool_value, Eq(kTestBool)), + Property(&Variant::vector, + ElementsAre( + Property(&Variant::int64_value, Eq(100L)), + Property(&Variant::string_value, StrEq("one hundred")), + Property(&Variant::double_value, Eq(100.0)), + Property(&Variant::vector, IsEmpty()), + Property(&Variant::is_null, Eq(true)), + Property(&Variant::int64_value, Eq(0)))), + Property(&Variant::double_value, Eq(kTestDouble)))); +} + +TEST_F(VariantTest, TestBasicMap) { + { + // Map of strings to Variant. + std::map m; + m["hello"] = kTestInt64; + m["world"] = kTestString; + m["how"] = kTestDouble; + m["are"] = kTestBool; + m["you"] = Variant(); + m["dude"] = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + Variant v(m); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_TRUE(v.is_container_type()); + EXPECT_FALSE(v.is_fundamental_type()); + EXPECT_THAT(v.map(), + UnorderedElementsAre( + Pair(Property(&Variant::string_value, StrEq("hello")), + Property(&Variant::int64_value, Eq(kTestInt64))), + Pair(Property(&Variant::string_value, StrEq("world")), + Property(&Variant::string_value, Eq(kTestString))), + Pair(Property(&Variant::string_value, StrEq("how")), + Property(&Variant::double_value, Eq(kTestDouble))), + Pair(Property(&Variant::string_value, StrEq("are")), + Property(&Variant::bool_value, Eq(kTestBool))), + Pair(Property(&Variant::string_value, StrEq("you")), + Property(&Variant::is_null, Eq(true))), + Pair(Property(&Variant::string_value, StrEq("dude")), + Property(&Variant::blob_size, Eq(kTestBlobSize))))); + } + { + std::map m; + m["0"] = kTestInt64; + m[0] = kTestString; + m[0.0] = kTestBool; + m[false] = kTestDouble; + m[Variant::Null()] = kTestMutableString; + Variant v(m); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT( + v.map(), + UnorderedElementsAre( + Pair(AllOf(Property(&Variant::is_string, Eq(true)), + Property(&Variant::string_value, StrEq("0"))), + AllOf(Property(&Variant::is_int64, Eq(true)), + Property(&Variant::int64_value, Eq(kTestInt64)))), + Pair(AllOf(Property(&Variant::is_int64, Eq(true)), + Property(&Variant::int64_value, Eq(0))), + AllOf(Property(&Variant::is_string, Eq(true)), + Property(&Variant::string_value, Eq(kTestString)))), + Pair(AllOf(Property(&Variant::is_double, Eq(true)), + Property(&Variant::double_value, Eq(0.0))), + AllOf(Property(&Variant::is_bool, Eq(true)), + Property(&Variant::bool_value, Eq(kTestBool)))), + Pair(AllOf(Property(&Variant::is_bool, Eq(true)), + Property(&Variant::bool_value, Eq(false))), + AllOf(Property(&Variant::is_double, Eq(true)), + Property(&Variant::double_value, Eq(kTestDouble)))), + Pair(Property(&Variant::is_null, Eq(true)), + AllOf(Property(&Variant::is_string, Eq(true)), + Property(&Variant::mutable_string, + Eq(kTestMutableString)))))); + } + { + // Ensure that if you reassign to a key in the map, it modifies it. + std::vector vect1 = {1, 2, 3, 4}; + std::vector vect2 = {1, 2, 4, 4}; + std::vector vect1copy = {1, 2, 3, 4}; + Variant v = Variant::EmptyMap(); + v.map()[vect1] = "Hello"; + v.map()[vect2] = "world"; + EXPECT_THAT(v.map(), + UnorderedElementsAre( + Pair(Property(&Variant::vector, ElementsAre(1, 2, 3, 4)), + Property(&Variant::string_value, StrEq("Hello"))), + Pair(Property(&Variant::vector, ElementsAre(1, 2, 4, 4)), + Property(&Variant::string_value, StrEq("world"))))); + EXPECT_THAT(vect1, Eq(vect1copy)); + v.map()[vect1copy] = "Goodbye"; + EXPECT_THAT(v.map(), + UnorderedElementsAre( + Pair(Property(&Variant::vector, ElementsAre(1, 2, 3, 4)), + Property(&Variant::string_value, StrEq("Goodbye"))), + Pair(Property(&Variant::vector, ElementsAre(1, 2, 4, 4)), + Property(&Variant::string_value, StrEq("world"))))); + } +} + +TEST_F(VariantTest, TestConstructingMapViaTemplate) { + { + std::map m{std::make_pair(23, "apple"), + std::make_pair(45, "banana"), + std::make_pair(67, "orange")}; + Variant v(m); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT( + v.map(), + UnorderedElementsAre( + Pair(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(23))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("apple")))), + Pair(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(45))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("banana")))), + Pair(AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(67))), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("orange")))))); + } +} + +TEST_F(VariantTest, TestNestedMaps) { + // TODO(jsimantov): Implement tests for maps of maps. +} + +TEST_F(VariantTest, TestComplexNesting) { + // TODO(jsimantov): Implement tests for complex nesting, e.g. maps of vectors + // of maps of etc. +} + +TEST_F(VariantTest, TestCopyAndAssignment) { + // Test copy constructor and assignment operator. + { + Variant v1(kTestString); + Variant v2(kTestInt64); + Variant v3(kTestMutableString); + Variant v4(kTestVector); + + EXPECT_THAT(v1.string_value(), Eq(kTestString)); + EXPECT_THAT(v2.int64_value(), Eq(kTestInt64)); + EXPECT_THAT(v3.mutable_string(), Eq(kTestMutableString)); + + v1 = v2; + EXPECT_THAT(v1.int64_value(), Eq(kTestInt64)); + EXPECT_THAT(v2.int64_value(), Eq(kTestInt64)); + + v1 = v3; + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1.mutable_string(), Eq(kTestMutableString)); + EXPECT_THAT(v3.mutable_string(), Eq(kTestMutableString)); + // Ensure they don't point to the same mutable string. + EXPECT_THAT(&v1.mutable_string(), Ne(&v3.mutable_string())); + + v1 = v4; + EXPECT_THAT(v1.vector(), Eq(kTestVector)); + EXPECT_THAT(v4.vector(), Eq(kTestVector)); + + Variant v5(kTestDouble); + Variant v6(v5); // NOLINT + EXPECT_THAT(v6, Eq(v5)); + + Variant v7(std::string("Mutable Longer string")); + Variant v8("Static"); + Variant v9(v7); + Variant v10(v8); // NOLINT + EXPECT_THAT(v7.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Mutable Longer string")); + v7 = v8; + EXPECT_THAT(v7.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Static")); + v7 = v9; + EXPECT_THAT(v7.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Mutable Longer string")); + v7 = v10; + EXPECT_THAT(v7.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v8.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v9.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v10.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v7.string_value(), StrEq("Static")); + } + + // Test move constructor. + { + Variant v1(kTestMutableString); + EXPECT_THAT(v1.mutable_string(), Eq(kTestMutableString)); + const std::string* v1_ptr = &v1.mutable_string(); + + Variant v2(std::move(v1)); + // Ensure v2 has the value that v1 had. + EXPECT_THAT(v2.mutable_string(), Eq(kTestMutableString)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::string* v2_ptr = &v2.mutable_string(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + + Variant v3(kTestVector); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeVector)); + v3 = std::move(v2); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v3.mutable_string(), Eq(kTestMutableString)); + EXPECT_TRUE(v2.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::string* v3_ptr = &v3.mutable_string(); + EXPECT_THAT(v2_ptr, Eq(v3_ptr)); + } + + { + Variant v = std::string("Hello"); + EXPECT_THAT(v, Eq("Hello")); + v = *&v; + EXPECT_THAT(v, Eq("Hello")); + Variant v1 = std::move(v); + v = std::move(v1); + EXPECT_THAT(v, Eq("Hello")); + } + + { + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + Variant v2 = Variant::FromMutableBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1, Eq(v2)); + Variant v3 = v1; + EXPECT_THAT(v1, Eq(v2)); + EXPECT_THAT(v1, Eq(v3)); + EXPECT_THAT(v2, Eq(v3)); + v3 = v2; + EXPECT_THAT(v1, Eq(v2)); + EXPECT_THAT(v1, Eq(v3)); + EXPECT_THAT(v2, Eq(v3)); + Variant v0 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + v3 = std::move(v1); + EXPECT_THAT(v3, Eq(v0)); + v3 = std::move(v2); + EXPECT_THAT(v3, Eq(v0)); + } +} + +TEST_F(VariantTest, TestEqualityOperators) { + { + Variant v0(3); + Variant v1(3); + Variant v2(4); + EXPECT_EQ(v0, v1); + EXPECT_NE(v1, v2); + EXPECT_NE(v0, v2); + EXPECT_TRUE(v0 < v2 || v2 < v0); + EXPECT_FALSE(v0 < v2 && v2 < v0); + + EXPECT_THAT(v0, Not(Lt(v1))); + EXPECT_THAT(v0, Not(Gt(v1))); + } + { + Variant v1("Hello, world!"); + Variant v2(std::string("Hello, world!")); + EXPECT_EQ(v1, v2); + } + { + Variant v1(std::vector{0, 1}); + Variant v2(std::vector{1, 0}); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v2.type(), Eq(Variant::kTypeVector)); + EXPECT_FALSE(v1 < v2 && v2 < v1); + } +} + +TEST_F(VariantTest, TestDefaults) { + EXPECT_THAT(Variant::Null(), + Property(&Variant::type, Eq(Variant::kTypeNull))); + EXPECT_THAT(Variant::Zero(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeInt64)), + Property(&Variant::int64_value, Eq(0)))); + EXPECT_THAT(Variant::ZeroPointZero(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeDouble)), + Property(&Variant::double_value, Eq(0.0)))); + EXPECT_THAT(Variant::False(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeBool)), + Property(&Variant::bool_value, Eq(false)))); + EXPECT_THAT(Variant::True(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeBool)), + Property(&Variant::bool_value, Eq(true)))); + EXPECT_THAT(Variant::EmptyString(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeStaticString)), + Property(&Variant::string_value, StrEq("")))); + EXPECT_THAT(Variant::EmptyMutableString(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeMutableString)), + Property(&Variant::string_value, StrEq("")))); + EXPECT_THAT(Variant::EmptyVector(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeVector)), + Property(&Variant::vector, IsEmpty()))); + EXPECT_THAT(Variant::EmptyMap(), + AllOf(Property(&Variant::type, Eq(Variant::kTypeMap)), + Property(&Variant::map, IsEmpty()))); +} + +TEST_F(VariantTest, TestSettersAndGetters) { + // TODO(jsimantov): Implement tests for setters and getters, including + // modifying the contents of Variant containers. Also verifies that const + // getters work, and are returning the same thing as non-const versions. + { + Variant v; + const Variant& vconst = v; + EXPECT_THAT(v.type(), Eq(Variant::kTypeNull)); + v.set_int64_value(123); + EXPECT_THAT(v.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v.int64_value(), Eq(123)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(vconst.int64_value(), Eq(123)); + EXPECT_EQ(v, vconst); + v.set_vector({4, 5, 6}); + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v.vector(), ElementsAre(4, 5, 6)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(vconst.vector(), ElementsAre(4, 5, 6)); + EXPECT_EQ(v, vconst); + v.set_double_value(456.7); + EXPECT_THAT(v.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v.double_value(), Eq(456.7)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(vconst.double_value(), Eq(456.7)); + EXPECT_EQ(v, vconst); + v.set_bool_value(false); + EXPECT_THAT(v.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v.bool_value(), Eq(false)); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(vconst.bool_value(), Eq(false)); + EXPECT_EQ(v, vconst); + v.set_map({std::make_pair(33, 44), std::make_pair(55, 66)}); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v.map(), UnorderedElementsAre(Pair(33, 44), Pair(55, 66))); + EXPECT_THAT(vconst.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(vconst.map(), UnorderedElementsAre(Pair(33, 44), Pair(55, 66))); + EXPECT_EQ(v, vconst); + } +} + +TEST_F(VariantTest, TestConversionFunctions) { + { + EXPECT_EQ(Variant::Null().AsBool(), Variant::False()); + EXPECT_EQ(Variant::Zero().AsBool(), Variant::False()); + EXPECT_EQ(Variant::ZeroPointZero().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyMap().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyVector().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyString().AsBool(), Variant::False()); + EXPECT_EQ(Variant::EmptyMutableString().AsBool(), Variant::False()); + + EXPECT_EQ(Variant::One().AsBool(), Variant::True()); + EXPECT_EQ(Variant::OnePointZero().AsBool(), Variant::True()); + EXPECT_EQ(Variant(123).AsBool(), Variant::True()); + EXPECT_EQ(Variant(456.7).AsBool(), Variant::True()); + EXPECT_EQ(Variant("Hello").AsBool(), Variant::True()); + EXPECT_EQ(Variant::MutableStringFromStaticString("Hello").AsBool(), + Variant::True()); + EXPECT_EQ(Variant(std::vector{0}).AsBool(), Variant::True()); + EXPECT_EQ(Variant(std::map{std::make_pair(23, "apple"), + std::make_pair(45, "banana"), + std::make_pair(67, "orange")}) + .AsBool(), + Variant::True()); + EXPECT_EQ(Variant::FromStaticBlob(kTestBlobData, 0).AsBool(), + Variant::False()); + EXPECT_EQ(Variant::FromMutableBlob(kTestBlobData, 0).AsBool(), + Variant::False()); + EXPECT_EQ(Variant::FromStaticBlob(kTestBlobData, kTestBlobSize).AsBool(), + Variant::True()); + EXPECT_EQ(Variant::FromMutableBlob(kTestBlobData, kTestBlobSize).AsBool(), + Variant::True()); + } + { + const Variant vint(12345); + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + + Variant vdouble = vint.AsDouble(); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(vdouble.double_value(), Eq(12345.0)); + + const Variant vstring("87755.899"); + EXPECT_TRUE(vstring.is_string()); + vdouble = vstring.AsDouble(); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(vdouble.double_value(), Eq(87755.899)); + + EXPECT_EQ(vdouble.AsDouble(), vdouble); + + EXPECT_THAT(Variant::True().AsDouble(), Eq(Variant(1.0))); + EXPECT_THAT(Variant::False().AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant::False().AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant::Null().AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant(kTestVector).AsDouble(), Eq(Variant::ZeroPointZero())); + EXPECT_THAT(Variant(g_test_map).AsDouble(), Eq(Variant::ZeroPointZero())); + } + { + Variant vstring(std::string("38294")); + EXPECT_TRUE(vstring.is_string()); + + Variant vint = vstring.AsInt64(); + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(vint.int64_value(), Eq(38294)); + + // Check truncation. + Variant vdouble(399.9); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + vint = vdouble.AsInt64(); + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(vint.int64_value(), Eq(399)); + + EXPECT_THAT(Variant::True().AsInt64(), Eq(Variant(1))); + EXPECT_THAT(Variant::False().AsInt64(), Eq(Variant::Zero())); + EXPECT_THAT(Variant::Null().AsInt64(), Eq(Variant::Zero())); + EXPECT_THAT(Variant(kTestVector).AsInt64(), Eq(Variant::Zero())); + EXPECT_THAT(Variant(g_test_map).AsInt64(), Eq(Variant::Zero())); + } + { + Variant vint(int64_t(9223372036800000000L)); // almost max value + EXPECT_THAT(vint.type(), Eq(Variant::kTypeInt64)); + + Variant vstring = vint.AsString(); + EXPECT_TRUE(vstring.is_string()); + EXPECT_THAT(vstring.string_value(), StrEq("9223372036800000000")); + + Variant vdouble(34491282.2909820005297661); + EXPECT_THAT(vdouble.type(), Eq(Variant::kTypeDouble)); + vstring = vdouble.AsString(); + EXPECT_TRUE(vstring.is_string()); + EXPECT_THAT(vstring.string_value(), StrEq("34491282.2909820005297661")); + + EXPECT_THAT(Variant::True().AsString(), Eq(Variant("true"))); + EXPECT_THAT(Variant::False().AsString(), Eq(Variant("false"))); + EXPECT_THAT(Variant::Null().AsString(), Eq(Variant::EmptyString())); + EXPECT_THAT(Variant(kTestVector).AsString(), Eq(Variant::EmptyString())); + EXPECT_THAT(Variant(g_test_map).AsString(), Eq(Variant::EmptyString())); + } +} + +// Copy a buffer+size into a vector, so gMock matchers can properly access it. +template +static std::vector AsVector(const T* buffer, size_t size_bytes) { + return std::vector(buffer, buffer + (size_bytes / sizeof(*buffer))); +} + +TEST_F(VariantTest, TestBlobs) { + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(v1.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(v1.blob_data(), Eq(kTestBlobData)); + + Variant v2 = Variant::FromMutableBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(v2.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(v2.blob_data(), Ne(kTestBlobData)); + + EXPECT_THAT(v1, Eq(v2)); + EXPECT_THAT(v1, Not(Lt(v2))); + EXPECT_THAT(v1, Not(Gt(v2))); + + // Make a copy of the mutable buffer that we can modify. + Variant v3 = v2; + + // Modify something within the mutable buffer, then ensure that they are + // no longer equal. Note that we don't care which is < the other. + reinterpret_cast(v3.mutable_blob_data())[kTestBlobSize / 2]++; + EXPECT_THAT(v1, Not(Eq(v3))); + EXPECT_THAT(v1, AnyOf(Lt(v3), Gt(v3))); + EXPECT_THAT(v2, Not(Eq(v3))); + EXPECT_THAT(v2, AnyOf(Lt(v3), Gt(v3))); + + // Ensure two blobs that are mostly the same but different sizes compare as + // different. + Variant v4 = Variant::FromMutableBlob(v2.blob_data(), v2.blob_size() - 1); + EXPECT_THAT(v2, Not(Eq(v4))); + EXPECT_THAT(v2, AnyOf(Lt(v4), Gt(v4))); + + // Check that two static blobs from the same data point to the same copy. + Variant v5 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v5.blob_data(), Eq(v1.blob_data())); + EXPECT_THAT(v5.blob_data(), Not(Eq(v2.blob_data()))); +} + +TEST_F(VariantTest, TestMutableBlobPromotion) { + Variant v = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + + EXPECT_THAT(v.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(v.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + (void)v.mutable_blob_data(); + EXPECT_THAT(v.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(v.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + // Modify one byte of the buffer. + reinterpret_cast(v.mutable_blob_data())[kTestBlobSize / 3] += 99; + uint8_t compare_buffer[kTestBlobSize]; + memcpy(compare_buffer, kTestBlobData, kTestBlobSize); + // Make the same change to a local buffer for comparison. + compare_buffer[kTestBlobSize / 3] += 99; + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(compare_buffer, kTestBlobSize)); + v.set_static_blob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v.blob_size(), Eq(kTestBlobSize)); + EXPECT_THAT(AsVector(v.blob_data(), v.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + + // Check that two static blobs from the same data point to the same copy, but + // not after promotion. + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + Variant v2 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.blob_data(), Eq(v2.blob_data())); + (void)v2.mutable_blob_data(); + EXPECT_THAT(v1.blob_data(), Ne(v2.blob_data())); + + // Check that you can call set_mutable_blob on a Variant's own blob_data and + // blob_size. + Variant v3 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(AsVector(v3.blob_data(), v3.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + v3.set_mutable_blob(v3.blob_data(), v3.blob_size()); + EXPECT_THAT(v3.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(AsVector(v3.blob_data(), v3.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); +} + +TEST_F(VariantTest, TestMoveConstructorOnAllTypes) { + // Test fundamental/statically allocated types. + { + Variant v1(kTestInt64); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v1.int64_value(), Eq(kTestInt64)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeInt64)); + EXPECT_THAT(v2.int64_value(), Eq(kTestInt64)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + Variant v1(kTestDouble); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v1.double_value(), Eq(kTestDouble)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeDouble)); + EXPECT_THAT(v2.double_value(), Eq(kTestDouble)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + Variant v1(kTestBool); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v1.bool_value(), Eq(kTestBool)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeBool)); + EXPECT_THAT(v2.bool_value(), Eq(kTestBool)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + // Static string. + Variant v1(kTestString); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v1.string_value(), Eq(kTestString)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeStaticString)); + EXPECT_THAT(v2.string_value(), Eq(kTestString)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + { + // Static blob. + Variant v1 = Variant::FromStaticBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(AsVector(v1.blob_data(), v1.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeStaticBlob)); + EXPECT_THAT(AsVector(v2.blob_data(), v2.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + } + + // Test allocated types (mutable string, blob, containers) + { + Variant v1(kTestMutableString); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v1.mutable_string(), Eq(kTestMutableString)); + const std::string* v1_ptr = &v1.mutable_string(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMutableString)); + EXPECT_THAT(v2.mutable_string(), Eq(kTestMutableString)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::string* v2_ptr = &v2.mutable_string(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + { + Variant v1(kTestVector); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v1.vector(), Eq(kTestVector)); + const std::vector* v1_ptr = &v1.vector(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v2.vector(), Eq(kTestVector)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::vector* v2_ptr = &v2.vector(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + { + Variant v1(g_test_map); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v1.map(), Eq(g_test_map)); + const std::map* v1_ptr = &v1.map(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v2.map(), Eq(g_test_map)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::map* v2_ptr = &v2.map(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + { + Variant v1 = Variant::FromMutableBlob(kTestBlobData, kTestBlobSize); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(AsVector(v1.blob_data(), v1.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + const void* v1_ptr = v1.blob_data(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMutableBlob)); + EXPECT_THAT(AsVector(v2.blob_data(), v2.blob_size()), + ElementsAreArray(kTestBlobData, kTestBlobSize)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const void* v2_ptr = v2.blob_data(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + // Test complex nested container type. + { + Variant v1(g_test_complex_map); + EXPECT_THAT(v1.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v1.map(), Eq(g_test_complex_map)); + const std::map* v1_ptr = &v1.map(); + Variant v2(std::move(v1)); + // Ensure v2 has the type and value that v1 had. + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v2.map(), Eq(g_test_complex_map)); + // Ensure v1 no longer has that value. + EXPECT_TRUE(v1.is_null()); // NOLINT + // Bonus points: Ensure that the pointer was simply moved. + const std::map* v2_ptr = &v2.map(); + EXPECT_THAT(v1_ptr, Eq(v2_ptr)); + } + + // Test moving over existing variant values. + { + Variant v2(kTestString); + Variant v1 = Variant::Null(); + v2 = std::move(v1); + EXPECT_TRUE(v1.is_null()); // NOLINT + EXPECT_TRUE(v2.is_null()); + } + { + Variant v2(g_test_complex_map); + EXPECT_THAT(v2.type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v2.map(), Eq(g_test_complex_map)); + Variant v1 = kTestComplexVector; + EXPECT_TRUE(v1.is_vector()); + EXPECT_THAT(v1.vector(), Eq(kTestComplexVector)); + v2 = std::move(v1); + EXPECT_TRUE(v1.is_null()); // NOLINT + EXPECT_TRUE(v2.is_vector()); + EXPECT_THAT(v2.vector(), Eq(kTestComplexVector)); + } + { + Variant v(kTestComplexVector); + EXPECT_THAT(v.type(), Eq(Variant::kTypeVector)); + EXPECT_THAT(v.vector(), Eq(kTestComplexVector)); + Variant v2(g_test_complex_map); + v.vector()[2] = std::move(v2); + EXPECT_THAT(v.vector()[2].type(), Eq(Variant::kTypeMap)); + EXPECT_THAT(v.vector()[2], Eq(g_test_complex_map)); + } +} + +} // namespace testing +} // namespace firebase diff --git a/app/tests/variant_util_test.cc b/app/tests/variant_util_test.cc new file mode 100644 index 0000000000..2c3e9196c0 --- /dev/null +++ b/app/tests/variant_util_test.cc @@ -0,0 +1,549 @@ +/* + * Copyright 2017 Google LLC + * + * 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 "app/src/variant_util.h" + +#include +#include + +#include "app/src/include/firebase/variant.h" +#include "app/tests/flexbuffer_matcher.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/json_util.h" + +namespace { + +using ::firebase::Variant; +using ::firebase::testing::cppsdk::EqualsJson; +using ::firebase::util::JsonToVariant; +using ::firebase::util::VariantToFlexbuffer; +using ::firebase::util::VariantToJson; +using ::flexbuffers::GetRoot; +using ::testing::Eq; +using ::testing::Not; +using ::testing::StrEq; + +TEST(UtilDesktopTest, JsonToVariantNull) { + EXPECT_THAT(JsonToVariant("null"), Eq(Variant::Null())); +} + +TEST(UtilDesktopTest, JsonToVariantInt64) { + EXPECT_THAT(JsonToVariant("0"), Eq(Variant(0))); + EXPECT_THAT(JsonToVariant("100"), Eq(Variant(100))); + EXPECT_THAT(JsonToVariant("8000000000"), Eq(Variant(int64_t(8000000000L)))); + EXPECT_THAT(JsonToVariant("-100"), Eq(Variant(-100))); + EXPECT_THAT(JsonToVariant("-8000000000"), Eq(Variant(int64_t(-8000000000L)))); +} + +TEST(UtilDesktopTest, JsonToVariantDouble) { + EXPECT_THAT(JsonToVariant("0.0"), Eq(Variant(0.0))); + EXPECT_THAT(JsonToVariant("100.0"), Eq(Variant(100.0))); + EXPECT_THAT(JsonToVariant("8000000000.0"), Eq(Variant(8000000000.0))); + EXPECT_THAT(JsonToVariant("-100.0"), Eq(Variant(-100.0))); + EXPECT_THAT(JsonToVariant("-8000000000.0"), Eq(Variant(-8000000000.0))); +} + +TEST(UtilDesktopTest, JsonToVariantBool) { + EXPECT_THAT(JsonToVariant("true"), Eq(Variant::True())); + EXPECT_THAT(JsonToVariant("false"), Eq(Variant::False())); +} + +TEST(UtilDesktopTest, JsonToVariantString) { + EXPECT_THAT(JsonToVariant("\"Hello, World!\""), Eq(Variant("Hello, World!"))); + EXPECT_THAT(JsonToVariant("\"100\""), Eq(Variant("100"))); + EXPECT_THAT(JsonToVariant("\"false\""), Eq(Variant("false"))); +} + +TEST(UtilDesktopTest, JsonToVariantVector) { + EXPECT_THAT(JsonToVariant("[]"), Eq(Variant::EmptyVector())); + std::vector int_vector{1, 2, 3, 4}; + EXPECT_THAT(JsonToVariant("[1, 2, 3, 4]"), Eq(Variant(int_vector))); + std::vector mixed_vector{1, true, 3.5, "hello"}; + EXPECT_THAT(JsonToVariant("[1, true, 3.5, \"hello\"]"), Eq(mixed_vector)); + std::vector nested_vector{1, true, 3.5, "hello", int_vector}; + EXPECT_THAT(JsonToVariant("[1, true, 3.5, \"hello\", [1, 2, 3, 4]]"), + Eq(nested_vector)); +} + +TEST(UtilDesktopTest, JsonToVariantMap) { + EXPECT_THAT(JsonToVariant("{}"), Eq(Variant::EmptyMap())); + std::map int_map{ + std::make_pair("one_hundred", 100), + std::make_pair("two_hundred", 200), + std::make_pair("three_hundred", 300), + std::make_pair("four_hundred", 400), + }; + EXPECT_THAT(JsonToVariant("{" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + "}"), + Eq(Variant(int_map))); + std::map mixed_map{ + std::make_pair("boolean_value", true), + std::make_pair("int_value", 100), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + }; + EXPECT_THAT(JsonToVariant("{" + " \"boolean_value\": true," + " \"int_value\": 100," + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + "}"), + Eq(mixed_map)); + std::map nested_map{ + std::make_pair("int_map", int_map), + std::make_pair("mixed_map", mixed_map), + }; + EXPECT_THAT(JsonToVariant("{" + " \"int_map\": {" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + " }," + " \"mixed_map\": {" + " \"int_value\": 100," + " \"boolean_value\": true, " + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + " }" + "}"), + Eq(nested_map)); +} + +TEST(UtilDesktopTest, VariantToJsonNull) { + EXPECT_THAT(VariantToJson(Variant::Null()), EqualsJson("null")); +} + +TEST(UtilDesktopTest, VariantToJsonInt64) { + EXPECT_THAT(VariantToJson(Variant(0)), EqualsJson("0")); + EXPECT_THAT(VariantToJson(Variant(100)), EqualsJson("100")); + EXPECT_THAT(VariantToJson(Variant(int64_t(8000000000L))), + EqualsJson("8000000000")); + EXPECT_THAT(VariantToJson(Variant(-100)), EqualsJson("-100")); + EXPECT_THAT(VariantToJson(Variant(int64_t(-8000000000L))), + EqualsJson("-8000000000")); +} + +TEST(UtilDesktopTest, VariantToJsonDouble) { + EXPECT_THAT(VariantToJson(Variant(0.0)), EqualsJson("0")); + EXPECT_THAT(VariantToJson(Variant(100.0)), EqualsJson("100")); + EXPECT_THAT(VariantToJson(Variant(-100.0)), EqualsJson("-100")); +} + +TEST(UtilDesktopTest, VariantToJsonBool) { + EXPECT_THAT(VariantToJson(Variant::True()), EqualsJson("true")); + EXPECT_THAT(VariantToJson(Variant::False()), EqualsJson("false")); +} + +TEST(UtilDesktopTest, VariantToJsonStaticString) { + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello, World!")), + EqualsJson("\"Hello, World!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("100")), + EqualsJson("\"100\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("false")), + EqualsJson("\"false\"")); +} + +TEST(UtilDesktopTest, VariantToJsonMutableString) { + EXPECT_THAT(VariantToJson(Variant::FromMutableString("Hello, World!")), + EqualsJson("\"Hello, World!\"")); + EXPECT_THAT(VariantToJson(Variant::FromMutableString("100")), + EqualsJson("\"100\"")); + EXPECT_THAT(VariantToJson(Variant::FromMutableString("false")), + EqualsJson("\"false\"")); +} + +TEST(UtilDesktopTest, VariantToJsonWithEscapeCharacters) { + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello, \"World\"!")), + EqualsJson("\"Hello, \\\"World\\\"!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello, \\backslash\\!")), + EqualsJson("\"Hello, \\\\backslash\\\\!\"")); + EXPECT_THAT( + VariantToJson(Variant::FromStaticString("Hello, // forwardslash!")), + EqualsJson("\"Hello, \\/\\/ forwardslash!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("Hello!\nHello again!")), + EqualsJson("\"Hello!\\nHello again!\"")); + EXPECT_THAT(VariantToJson(Variant::FromStaticString("こんにちは")), + EqualsJson("\"\\u3053\\u3093\\u306B\\u3061\\u306F\"")); +} + +TEST(UtilDesktopTest, VariantToJsonVector) { + EXPECT_THAT(VariantToJson(Variant::EmptyVector()), EqualsJson("[]")); + EXPECT_THAT(VariantToJson(Variant::EmptyVector(), true), EqualsJson("[]")); + + std::vector int_vector{1, 2, 3, 4}; + EXPECT_THAT(VariantToJson(Variant(int_vector)), StrEq("[1,2,3,4]")); + EXPECT_THAT(VariantToJson(Variant(int_vector), true), + StrEq("[\n 1,\n 2,\n 3,\n 4\n]")); + + std::vector mixed_vector{1, true, 3.5, "hello"}; + EXPECT_THAT(VariantToJson(Variant(mixed_vector)), + StrEq("[1,true,3.5,\"hello\"]")); + EXPECT_THAT(VariantToJson(Variant(mixed_vector), true), + StrEq("[\n 1,\n true,\n 3.5,\n \"hello\"\n]")); + + std::vector nested_vector{1, true, 3.5, "hello", int_vector}; + EXPECT_THAT(VariantToJson(nested_vector), + StrEq("[1,true,3.5,\"hello\",[1,2,3,4]]")); + EXPECT_THAT(VariantToJson(nested_vector, true), + StrEq("[\n 1,\n true,\n 3.5,\n \"hello\",\n" + " [\n 1,\n 2,\n 3,\n 4\n ]\n]")); +} + +TEST(UtilDesktopTest, VariantToJsonMapWithStringKeys) { + EXPECT_THAT(VariantToJson(Variant::EmptyMap()), EqualsJson("{}")); + std::map int_map{ + std::make_pair("one_hundred", 100), + std::make_pair("two_hundred", 200), + std::make_pair("three_hundred", 300), + std::make_pair("four_hundred", 400), + }; + EXPECT_THAT(VariantToJson(Variant(int_map)), + EqualsJson("{" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + "}")); + std::map mixed_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + }; + EXPECT_THAT(VariantToJson(mixed_map), + EqualsJson("{" + " \"int_value\": 100," + " \"boolean_value\": true," + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + "}")); + std::map nested_map{ + std::make_pair("int_map", int_map), + std::make_pair("mixed_map", mixed_map), + }; + EXPECT_THAT(VariantToJson(nested_map), + EqualsJson("{" + " \"int_map\":{" + " \"one_hundred\": 100," + " \"two_hundred\": 200," + " \"three_hundred\": 300," + " \"four_hundred\": 400" + " }," + " \"mixed_map\":{" + " \"int_value\": 100," + " \"boolean_value\": true," + " \"double_value\": 3.5," + " \"string_value\": \"Good-bye, World!\"" + " }" + "}")); + + // Test pretty printing with one key per map, since key order may vary. + std::map nested_one_key_map{ + std::make_pair("a", + std::vector{ + 1, 2, + std::map{ + std::make_pair("b", std::vector{3, 4}), + }}), + }; + EXPECT_THAT(VariantToJson(Variant(nested_one_key_map), true), + StrEq("{\n" + " \"a\": [\n" + " 1,\n" + " 2,\n" + " {\n" + " \"b\": [\n" + " 3,\n" + " 4\n" + " ]\n" + " }\n" + " ]\n" + "}")); +} + +TEST(UtilDesktopTest, VariantToJsonMapLegalNonStringKeys) { + // VariantToJson will convert fundamental types to strings. + std::map int_key_map{ + std::make_pair(100, "one_hundred"), + std::make_pair(200, "two_hundred"), + std::make_pair(300, "three_hundred"), + std::make_pair(400, "four_hundred"), + }; + EXPECT_THAT(VariantToJson(Variant(int_key_map)), + EqualsJson("{" + " \"100\": \"one_hundred\"," + " \"200\": \"two_hundred\"," + " \"300\": \"three_hundred\"," + " \"400\": \"four_hundred\"" + "}")); + std::map mixed_key_map{ + std::make_pair(100, "int_value"), + std::make_pair(3.5, "double_value"), + std::make_pair(true, "boolean_value"), + std::make_pair("Good-bye, World!", "string_value"), + }; + EXPECT_THAT(VariantToJson(mixed_key_map), + EqualsJson("{" + " \"100\": \"int_value\"," + " \"3.5000000000000000\": \"double_value\"," + " \"true\": \"boolean_value\"," + " \"Good-bye, World!\": \"string_value\"" + "}")); +} + +TEST(UtilDesktopTest, VariantToJsonMapWithBadKeys) { + // JSON only supports strings for keys (and this implmentation will coerce + // fundamental types to string keys. Anything else (containers, blobs) + // should fail, which is represented by an empty string. Also, the empty + // string is not valid JSON, so we must test with StrEq instead of + // JsonEquals. + + // Vector as a key. + std::vector int_vector{1, 2, 3, 4}; + std::map map_with_vector_key{ + std::make_pair(int_vector, "pairs of numbers!"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_vector_key)), StrEq("")); + + // Map as a key. + std::map int_map{ + std::make_pair(1, 1), + std::make_pair(2, 3), + std::make_pair(5, 8), + std::make_pair(13, 21), + }; + std::map map_with_map_key{ + std::make_pair(int_map, "pairs of numbers!"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_map_key)), StrEq("")); + + std::string blob_data = "abcdefghijklmnopqrstuvwxyz"; + + // Static blob as a key. + Variant static_blob = + Variant::FromStaticBlob(blob_data.c_str(), blob_data.size()); + std::map map_with_static_blob_key{ + std::make_pair(static_blob, "blobby blob blob"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_static_blob_key)), StrEq("")); + + // Mutable blob as a key. + Variant mutable_blob = + Variant::FromMutableBlob(blob_data.c_str(), blob_data.size()); + std::map map_with_mutable_blob_key{ + std::make_pair(static_blob, "blorby blorb blorb"), + }; + EXPECT_THAT(VariantToJson(Variant(map_with_mutable_blob_key)), StrEq("")); + + // Legal top level map with illegal nested values. + std::map map_with_legal_key{ + std::make_pair("totes legal", map_with_map_key)}; + EXPECT_THAT(VariantToJson(Variant(map_with_legal_key)), StrEq("")); +} + +TEST(UtilDesktopTest, VariantToJsonWithStaticBlob) { + // Static blobs are not supported, so we expect these to fail, which is + // represented by an empty string. + std::string blob_data = "abcdefghijklmnopqrstuvwxyz"; + Variant blob = Variant::FromStaticBlob(blob_data.c_str(), blob_data.size()); + EXPECT_THAT(VariantToJson(blob), StrEq("")); + std::vector blob_vector{1, true, 3.5, "hello", blob}; + EXPECT_THAT(VariantToJson(blob_vector), StrEq("")); + std::map blob_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + std::make_pair("blob_value", blob), + }; + EXPECT_THAT(VariantToJson(blob_map), StrEq("")); +} + +TEST(UtilDesktopTest, VariantToJsonWithMutableBlob) { + // Mutable blobs are not supported, so we expect these to fail, which is + // represented by an empty string. + std::string blob_data = "abcdefghijklmnopqrstuvwxyz"; + Variant blob = Variant::FromMutableBlob(blob_data.c_str(), blob_data.size()); + EXPECT_THAT(VariantToJson(blob), StrEq("")); + std::vector blob_vector{1, true, 3.5, "hello", blob}; + EXPECT_THAT(VariantToJson(blob_vector), StrEq("")); + std::map blob_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + std::make_pair("blob_value", blob), + }; + EXPECT_THAT(VariantToJson(blob_map), StrEq("")); +} + +TEST(UtilDesktopTest, VariantToFlexbufferNull) { + EXPECT_TRUE(GetRoot(VariantToFlexbuffer(Variant::Null())).IsNull()); +} + +TEST(UtilDesktopTest, VariantToFlexbufferInt64) { + EXPECT_THAT(GetRoot(VariantToFlexbuffer(0)).AsInt32(), Eq(0)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(100)).AsInt32(), Eq(100)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(int64_t(8000000000L))).AsInt64(), + Eq(8000000000)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(-100)).AsInt32(), Eq(-100)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(int64_t(-8000000000L))).AsInt64(), + Eq(-8000000000)); +} + +TEST(UtilDesktopTest, VariantToFlexbufferDouble) { + EXPECT_THAT(GetRoot(VariantToFlexbuffer(0.0)).AsDouble(), Eq(0.0)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(100.0)).AsDouble(), Eq(100.0)); + EXPECT_THAT(GetRoot(VariantToFlexbuffer(-100.0)).AsDouble(), Eq(-100.0)); +} + +TEST(UtilDesktopTest, VariantToFlexbufferBool) { + EXPECT_TRUE(GetRoot(VariantToFlexbuffer(Variant::True())).AsBool()); + EXPECT_FALSE(GetRoot(VariantToFlexbuffer(Variant::False())).AsBool()); +} + +TEST(UtilDesktopTest, VariantToFlexbufferString) { + EXPECT_THAT(GetRoot(VariantToFlexbuffer("Hello, World!")).AsString().c_str(), + StrEq("Hello, World!")); + EXPECT_THAT(GetRoot(VariantToFlexbuffer("100")).AsString().c_str(), + StrEq("100")); + EXPECT_THAT(GetRoot(VariantToFlexbuffer("false")).AsString().c_str(), + StrEq("false")); +} + +TEST(UtilDesktopTest, VariantToFlexbufferVector) { + flexbuffers::Builder fbb(512); + fbb.Vector([&]() {}); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(Variant::EmptyVector()), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::vector int_vector{1, 2, 3, 4}; + fbb.Vector([&]() { + fbb += 1; + fbb += 2; + fbb += 3; + fbb += 4; + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(int_vector), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::vector mixed_vector{1, true, 3.5, "hello"}; + fbb.Vector([&]() { + fbb += 1; + fbb += true; + fbb += 3.5; + fbb += "hello"; + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(mixed_vector), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::vector nested_vector{1, true, 3.5, "hello", int_vector}; + fbb.Vector([&]() { + fbb += 1; + fbb += true; + fbb += 3.5; + fbb += "hello"; + fbb.Vector([&]() { + fbb += 1; + fbb += 2; + fbb += 3; + fbb += 4; + }); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(nested_vector), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); +} + +TEST(UtilDesktopTest, VariantToFlexbufferMapWithStringKeys) { + flexbuffers::Builder fbb(512); + fbb.Map([&]() {}); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(Variant::EmptyMap()), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::map int_map{ + std::make_pair("one_hundred", 100), + std::make_pair("two_hundred", 200), + std::make_pair("three_hundred", 300), + std::make_pair("four_hundred", 400), + }; + fbb.Map([&]() { + fbb.Add("one_hundred", 100); + fbb.Add("two_hundred", 200); + fbb.Add("three_hundred", 300); + fbb.Add("four_hundred", 400); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(int_map), EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::map mixed_map{ + std::make_pair("int_value", 100), + std::make_pair("boolean_value", true), + std::make_pair("double_value", 3.5), + std::make_pair("string_value", "Good-bye, World!"), + }; + fbb.Map([&]() { + fbb.Add("int_value", 100); + fbb.Add("boolean_value", true); + fbb.Add("double_value", 3.5); + fbb.Add("string_value", "Good-bye, World!"); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(mixed_map), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); + + std::map nested_map{ + std::make_pair("int_map", int_map), + std::make_pair("mixed_map", mixed_map), + }; + fbb.Map([&]() { + fbb.Map("int_map", [&]() { + fbb.Add("one_hundred", 100); + fbb.Add("two_hundred", 200); + fbb.Add("three_hundred", 300); + fbb.Add("four_hundred", 400); + }); + fbb.Map("mixed_map", [&]() { + fbb.Add("int_value", 100); + fbb.Add("boolean_value", true); + fbb.Add("double_value", 3.5); + fbb.Add("string_value", "Good-bye, World!"); + }); + }); + fbb.Finish(); + EXPECT_THAT(VariantToFlexbuffer(nested_map), + EqualsFlexbuffer(fbb.GetBuffer())); + fbb.Clear(); +} + +} // namespace diff --git a/auth/src/ios/fake/FIRActionCodeSettings.h b/auth/src/ios/fake/FIRActionCodeSettings.h new file mode 100644 index 0000000000..cb7528cc7f --- /dev/null +++ b/auth/src/ios/fake/FIRActionCodeSettings.h @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Google + * + * 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/LICENSE2.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. + */ + + #import + + NS_ASSUME_NONNULL_BEGIN + + /** @class FIRActionCodeSettings + @brief Used to set and retrieve settings related to handling action codes. + */ + NS_SWIFT_NAME(ActionCodeSettings) + @interface FIRActionCodeSettings : NSObject + + /** @property URL + @brief This URL represents the state/Continue URL in the form of a universal link. + @remarks This URL can should be contructed as a universal link that would either directly open + the app where the action code would be handled or continue to the app after the action code + is handled by Firebase. + */ + @property(nonatomic, copy, nullable) NSURL *URL; + + /** @property handleCodeInApp + @brief Indicates whether the action code link will open the app directly or after being + redirected from a Firebase owned web widget. + */ + @property(assign, nonatomic) BOOL handleCodeInApp; + + /** @property iOSBundleID + @brief The iOS bundle ID, if available. The default value is the current app's bundle ID. + */ + @property(copy, nonatomic, readonly, nullable) NSString *iOSBundleID; + + /** @property androidPackageName + @brief The Android package name, if available. + */ + @property(nonatomic, copy, readonly, nullable) NSString *androidPackageName; + + /** @property androidMinimumVersion + @brief The minimum Android version supported, if available. + */ + @property(nonatomic, copy, readonly, nullable) NSString *androidMinimumVersion; + + /** @property androidInstallIfNotAvailable + @brief Indicates whether the Android app should be installed on a device where it is not + available. + */ + @property(nonatomic, assign, readonly) BOOL androidInstallIfNotAvailable; + + /** @property dynamicLinkDomain + @brief The Firebase Dynamic Link domain used for out of band code flow. + */ + @property(copy, nonatomic, nullable) NSString *dynamicLinkDomain; + + /** @fn setIOSBundleID + @brief Sets the iOS bundle Id. + @param iOSBundleID The iOS bundle ID. + */ + - (void)setIOSBundleID:(NSString *)iOSBundleID; + + /** @fn setAndroidPackageName:installIfNotAvailable:minimumVersion: + @brief Sets the Android package name, the flag to indicate whether or not to install the app + and the minimum Android version supported. + @param androidPackageName The Android package name. + @param installIfNotAvailable Indicates whether or not the app should be installed if not + available. + @param minimumVersion The minimum version of Android supported. + @remarks If installIfNotAvailable is set to YES and the link is opened on an android device, it + will try to install the app if not already available. Otherwise the web URL is used. + */ + - (void)setAndroidPackageName:(NSString *)androidPackageName + installIfNotAvailable:(BOOL)installIfNotAvailable + minimumVersion:(nullable NSString *)minimumVersion; + + @end + + NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAdditionalUserInfo.h b/auth/src/ios/fake/FIRAdditionalUserInfo.h new file mode 100644 index 0000000000..2e57ff20f4 --- /dev/null +++ b/auth/src/ios/fake/FIRAdditionalUserInfo.h @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRVerifyAssertionResponse; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAdditionalUserInfo + @brief Represents additional user data returned from an identity provider. + */ +NS_SWIFT_NAME(AdditionalUserInfo) +@interface FIRAdditionalUserInfo : NSObject + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be initialized manually. `FIRAdditionalUserInfo` can be retrieved + from from an instance of `FIRAuthDataResult`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @property providerID + @brief The provider identifier. + */ +@property(nonatomic, readonly) NSString *providerID; + +/** @property profile + @brief Dictionary containing the additional IdP specific information. + */ +@property(nonatomic, readonly, nullable) NSDictionary *profile; + +/** @property username + @brief username The name of the user. + */ +@property(nonatomic, readonly, nullable) NSString *username; + +/** @property newUser + @brief Indicates whether or not the current user was signed in for the first time. + */ +@property(nonatomic, readonly, getter=isNewUser) BOOL newUser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAdditionalUserInfo.mm b/auth/src/ios/fake/FIRAdditionalUserInfo.mm new file mode 100644 index 0000000000..1e37b3fa8e --- /dev/null +++ b/auth/src/ios/fake/FIRAdditionalUserInfo.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAdditionalUserInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAdditionalUserInfo + +- (instancetype)init { + self = [super init]; + if (self) { + _providerID = @"fake provider id"; + _profile = nil; + _username = @"fake user name"; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuth.h b/auth/src/ios/fake/FIRAuth.h new file mode 100644 index 0000000000..29cf4320a6 --- /dev/null +++ b/auth/src/ios/fake/FIRAuth.h @@ -0,0 +1,832 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import +#import + +#if TARGET_OS_IOS +#import "auth/src/ios/fake/FIRAuthAPNSTokenType.h" +#endif + +@class FIRActionCodeSettings; +@class FIRApp; +@class FIRAuth; +@class FIRAuthCredential; +@class FIRAuthDataResult; +@class FIRAuthSettings; +@class FIRUser; +@protocol FIRAuthStateListener; +@protocol FIRAuthUIDelegate; +@protocol FIRFederatedAuthProvider; + +NS_ASSUME_NONNULL_BEGIN + +/** @typedef FIRUserUpdateCallback + @brief The type of block invoked when a request to update the current user is completed. + */ +typedef void (^FIRUserUpdateCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(UserUpdateCallback); + +/** @typedef FIRAuthStateDidChangeListenerHandle + @brief The type of handle returned by `FIRAuth.addAuthStateDidChangeListener:`. + */ +typedef id FIRAuthStateDidChangeListenerHandle + NS_SWIFT_NAME(AuthStateDidChangeListenerHandle); + +/** @typedef FIRAuthStateDidChangeListenerBlock + @brief The type of block which can be registered as a listener for auth state did change events. + + @param auth The FIRAuth object on which state changes occurred. + @param user Optionally; the current signed in user, if any. + */ +typedef void(^FIRAuthStateDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user) + NS_SWIFT_NAME(AuthStateDidChangeListenerBlock); + +/** @typedef FIRIDTokenDidChangeListenerHandle + @brief The type of handle returned by `FIRAuth.addIDTokenDidChangeListener:`. + */ +typedef id FIRIDTokenDidChangeListenerHandle + NS_SWIFT_NAME(IDTokenDidChangeListenerHandle); + +/** @typedef FIRIDTokenDidChangeListenerBlock + @brief The type of block which can be registered as a listener for ID token did change events. + + @param auth The FIRAuth object on which ID token changes occurred. + @param user Optionally; the current signed in user, if any. + */ +typedef void(^FIRIDTokenDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user) + NS_SWIFT_NAME(IDTokenDidChangeListenerBlock); + +/** @typedef FIRAuthDataResultCallback + @brief The type of block invoked when sign-in related events complete. + + @param authResult Optionally; Result of sign-in request containing both the user and + the additional user info associated with the user. + @param error Optionally; the error which occurred - or nil if the request was successful. + */ +typedef void (^FIRAuthDataResultCallback)(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthDataResultCallback); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + @brief The name of the `NSNotificationCenter` notification which is posted when the auth state + changes (for example, a new token has been produced, a user signs in or signs out). The + object parameter of the notification is the sender `FIRAuth` instance. + */ +extern const NSNotificationName FIRAuthStateDidChangeNotification + NS_SWIFT_NAME(AuthStateDidChange); +#else +/** + @brief The name of the `NSNotificationCenter` notification which is posted when the auth state + changes (for example, a new token has been produced, a user signs in or signs out). The + object parameter of the notification is the sender `FIRAuth` instance. + */ +extern NSString *const FIRAuthStateDidChangeNotification + NS_SWIFT_NAME(AuthStateDidChangeNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** @typedef FIRAuthResultCallback + @brief The type of block invoked when sign-in related events complete. + + @param user Optionally; the signed in user, if any. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRAuthResultCallback)(FIRUser *_Nullable user, NSError *_Nullable error) + NS_SWIFT_NAME(AuthResultCallback); + +/** @typedef FIRProviderQueryCallback + @brief The type of block invoked when a list of identity providers for a given email address is + requested. + + @param providers Optionally; a list of provider identifiers, if any. + @see FIRGoogleAuthProviderID etc. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRProviderQueryCallback)(NSArray *_Nullable providers, + NSError *_Nullable error) + NS_SWIFT_NAME(ProviderQueryCallback); + +/** @typedef FIRSignInMethodQueryCallback + @brief The type of block invoked when a list of sign-in methods for a given email address is + requested. + */ +typedef void (^FIRSignInMethodQueryCallback)(NSArray *_Nullable, + NSError *_Nullable) + NS_SWIFT_NAME(SignInMethodQueryCallback); + +/** @typedef FIRSendPasswordResetCallback + @brief The type of block invoked when sending a password reset email. + + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRSendPasswordResetCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendPasswordResetCallback); + +/** @typedef FIRSendSignInLinkToEmailCallback + @brief The type of block invoked when sending an email sign-in link email. + */ +typedef void (^FIRSendSignInLinkToEmailCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendSignInLinkToEmailCallback); + +/** @typedef FIRConfirmPasswordResetCallback + @brief The type of block invoked when performing a password reset. + + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRConfirmPasswordResetCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(ConfirmPasswordResetCallback); + +/** @typedef FIRVerifyPasswordResetCodeCallback + @brief The type of block invoked when verifying that an out of band code should be used to + perform password reset. + + @param email Optionally; the email address of the user for which the out of band code applies. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRVerifyPasswordResetCodeCallback)(NSString *_Nullable email, + NSError *_Nullable error) + NS_SWIFT_NAME(VerifyPasswordResetCodeCallback); + +/** @typedef FIRApplyActionCodeCallback + @brief The type of block invoked when applying an action code. + + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRApplyActionCodeCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(ApplyActionCodeCallback); + +/** + @brief Keys used to retrieve operation data from a `FIRActionCodeInfo` object by the + `dataForKey` method. + */ +typedef NS_ENUM(NSInteger, FIRActionDataKey) { + /** + * The email address to which the code was sent. + * For FIRActionCodeOperationRecoverEmail, the new email address for the account. + */ + FIRActionCodeEmailKey = 0, + + /** For FIRActionCodeOperationRecoverEmail, the current email address for the account. */ + FIRActionCodeFromEmailKey = 1 +} NS_SWIFT_NAME(ActionDataKey); + +/** @class FIRActionCodeInfo + @brief Manages information regarding action codes. + */ +NS_SWIFT_NAME(ActionCodeInfo) +@interface FIRActionCodeInfo : NSObject + +/** + @brief Operations which can be performed with action codes. + */ +typedef NS_ENUM(NSInteger, FIRActionCodeOperation) { + /** Action code for unknown operation. */ + FIRActionCodeOperationUnknown = 0, + + /** Action code for password reset operation. */ + FIRActionCodeOperationPasswordReset = 1, + + /** Action code for verify email operation. */ + FIRActionCodeOperationVerifyEmail = 2, + + /** Action code for recover email operation. */ + FIRActionCodeOperationRecoverEmail = 3, + + /** Action code for email link operation. */ + FIRActionCodeOperationEmailLink = 4, + + +} NS_SWIFT_NAME(ActionCodeOperation); + +/** + @brief The operation being performed. + */ +@property(nonatomic, readonly) FIRActionCodeOperation operation; + +/** @fn dataForKey: + @brief The operation being performed. + + @param key The FIRActionDataKey value used to retrieve the operation data. + + @return The operation data pertaining to the provided action code key. + */ +- (NSString *)dataForKey:(FIRActionDataKey)key; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief please use initWithOperation: instead. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +@end + +/** @typedef FIRCheckActionCodeCallBack + @brief The type of block invoked when performing a check action code operation. + + @param info Metadata corresponding to the action code. + @param error Optionally; if an error occurs, this is the NSError object that describes the + problem. Set to nil otherwise. + */ +typedef void (^FIRCheckActionCodeCallBack)(FIRActionCodeInfo *_Nullable info, + NSError *_Nullable error) + NS_SWIFT_NAME(CheckActionCodeCallback); + +/** @class FIRAuth + @brief Manages authentication for Firebase apps. + @remarks This class is thread-safe. + */ +NS_SWIFT_NAME(Auth) +@interface FIRAuth : NSObject + +/** @fn auth + @brief Gets the auth object for the default Firebase app. + @remarks The default Firebase app must have already been configured or an exception will be + raised. + */ ++ (FIRAuth *)auth NS_SWIFT_NAME(auth()); + +/** @fn authWithApp: + @brief Gets the auth object for a `FIRApp`. + + @param app The FIRApp for which to retrieve the associated FIRAuth instance. + @return The FIRAuth instance associated with the given FIRApp. + */ ++ (FIRAuth *)authWithApp:(FIRApp *)app NS_SWIFT_NAME(auth(app:)); + +/** @property app + @brief Gets the `FIRApp` object that this auth object is connected to. + */ +@property(nonatomic, weak, readonly, nullable) FIRApp *app; + +/** @property currentUser + @brief Synchronously gets the cached current user, or null if there is none. + */ +@property(nonatomic, strong, readonly, nullable) FIRUser *currentUser; + +/** @property languageCode + @brief The current user language code. This property can be set to the app's current language by + calling `useAppLanguage`. + + @remarks The string used to set this property must be a language code that follows BCP 47. + */ +@property(nonatomic, copy, nullable) NSString *languageCode; + +/** @property settings + @brief Contains settings related to the auth object. + */ +@property(nonatomic, copy, nullable) FIRAuthSettings *settings; + +/** @property userAccessGroup + @brief The current user access group that the Auth instance is using. Default is nil. + */ +@property(readonly, nonatomic, copy, nullable) NSString *userAccessGroup; + +#if TARGET_OS_IOS +/** @property APNSToken + @brief The APNs token used for phone number authentication. The type of the token (production + or sandbox) will be attempted to be automatcially detected. + @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work, + by either setting this property or by calling `setAPNSToken:type:` + */ +@property(nonatomic, strong, nullable) NSData *APNSToken; +#endif + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief Please access auth instances using `FIRAuth.auth` and `FIRAuth.authForApp:`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @fn updateCurrentUser:completion: + @brief Sets the currentUser on the calling Auth instance to the provided user object. + @param user The user object to be set as the current user of the calling Auth instance. + @param completion Optionally; a block invoked after the user of the calling Auth instance has + been updated or an error was encountered. + */ +- (void)updateCurrentUser:(FIRUser *)user completion:(nullable FIRUserUpdateCallback)completion; + +/** @fn fetchProvidersForEmail:completion: + @brief Please use fetchSignInMethodsForEmail:completion: for Objective-C or + fetchSignInMethods(forEmail:completion:) for Swift instead. + */ +- (void)fetchProvidersForEmail:(NSString *)email + completion:(nullable FIRProviderQueryCallback)completion +DEPRECATED_MSG_ATTRIBUTE("Please use fetchSignInMethodsForEmail:completion: for Objective-C or " + "fetchSignInMethods(forEmail:completion:) for Swift instead."); + +/** @fn fetchSignInMethodsForEmail:completion: + @brief Fetches the list of all sign-in methods previously used for the provided email address. + + @param email The email address for which to obtain a list of sign-in methods. + @param completion Optionally; a block which is invoked when the list of sign in methods for the + specified email address is ready or an error was encountered. Invoked asynchronously on the + main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods. + */ + +- (void)fetchSignInMethodsForEmail:(NSString *)email + completion:(nullable FIRSignInMethodQueryCallback)completion; + +/** @fn signInWithEmail:password:completion: + @brief Signs in using an email address and password. + + @param email The user's email address. + @param password The user's password. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and password + accounts are not enabled. Enable them in the Auth section of the + Firebase console. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted + sign in with an incorrect password. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)signInWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInWithEmail:link:completion: + @brief Signs in using an email address and email sign-in link. + + @param email The user's email address. + @param link The email sign-in link. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and email sign-in link + accounts are not enabled. Enable them in the Auth section of the + Firebase console. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is invalid. + + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ + +- (void)signInWithEmail:(NSString *)email + link:(NSString *)link + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInWithProvider:UIDelegate:completion: + @brief Signs in using the provided auth provider instance. + + @param provider An instance of an auth provider used to initiate the sign-in flow. + @param UIDelegate Optionally an instance of a class conforming to the FIRAuthUIDelegate + protocol, this is used for presenting the web context. If nil, a default FIRAuthUIDelegate + will be used. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: +
      +
    • @c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password + accounts are not enabled. Enable them in the Auth section of the + Firebase console. +
    • +
    • @c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled. +
    • +
    • @c FIRAuthErrorCodeWebNetworkRequestFailed - Indicates that a network request within a + SFSafariViewController or UIWebview failed. +
    • +
    • @c FIRAuthErrorCodeWebInternalError - Indicates that an internal error occurred within a + SFSafariViewController or UIWebview. +
    • +
    • @c FIRAuthErrorCodeWebSignInUserInteractionFailure - Indicates a general failure during + a web sign-in flow. +
    • +
    • @c FIRAuthErrorCodeWebContextAlreadyPresented - Indicates that an attempt was made to + present a new web context while one was already being presented. +
    • +
    • @c FIRAuthErrorCodeWebContextCancelled - Indicates that the URL presentation was + cancelled prematurely by the user. +
    • +
    • @c FIRAuthErrorCodeAccountExistsWithDifferentCredential - Indicates the email asserted + by the credential (e.g. the email in a Facebook access token) is already in use by an + existing account, that cannot be authenticated with this sign-in method. Call + fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + the sign-in providers returned. This error will only be thrown if the "One account per + email address" setting is enabled in the Firebase console, under Auth settings. +
    • +
    + + @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods. + */ +- (void)signInWithProvider:(id)provider + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInAndRetrieveDataWithCredential:completion: + @brief Please use signInWithCredential:completion: for Objective-C or " + "signIn(with:completion:) for Swift instead. + */ +- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion +DEPRECATED_MSG_ATTRIBUTE("Please use signInWithCredential:completion: for Objective-C or " + "signIn(with:completion:) for Swift instead."); + +/** @fn signInWithCredential:completion: + @brief Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook + login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional + identity provider data. + + @param credential The credential supplied by the IdP. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + This could happen if it has expired or it is malformed. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that accounts + with the identity provider represented by the credential are not enabled. + Enable them in the Auth section of the Firebase console. + + `FIRAuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted + by the credential (e.g. the email in a Facebook access token) is already in use by an + existing account, that cannot be authenticated with this sign-in method. Call + fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + the sign-in providers returned. This error will only be thrown if the "One account per + email address" setting is enabled in the Firebase console, under Auth settings. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted sign in with an + incorrect password, if credential is of the type EmailPasswordAuthCredential. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + `FIRAuthErrorCodeMissingVerificationID` - Indicates that the phone auth credential was + created with an empty verification ID. + + `FIRAuthErrorCodeMissingVerificationCode` - Indicates that the phone auth credential + was created with an empty verification code. + + `FIRAuthErrorCodeInvalidVerificationCode` - Indicates that the phone auth credential + was created with an invalid verification Code. + + `FIRAuthErrorCodeInvalidVerificationID` - Indicates that the phone auth credential was + created with an invalid verification ID. + + `FIRAuthErrorCodeSessionExpired` - Indicates that the SMS code has expired. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods +*/ +- (void)signInWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInAnonymouslyWithCompletion: + @brief Asynchronously creates and becomes an anonymous user. + @param completion Optionally; a block which is invoked when the sign in finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks If there is already an anonymous user signed in, that user will be returned instead. + If there is any other existing user signed in, that user will be signed out. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are + not enabled. Enable them in the Auth section of the Firebase console. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn signInWithCustomToken:completion: + @brief Asynchronously signs in to Firebase with the given Auth token. + + @param token A self-signed custom auth token. + @param completion Optionally; a block which is invoked when the sign in finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidCustomToken` - Indicates a validation error with + the custom token. + + `FIRAuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key + belong to different projects. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)signInWithCustomToken:(NSString *)token + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn createUserWithEmail:password:completion: + @brief Creates and, on success, signs in a user with the given email address and password. + + @param email The user's email address. + @param password The user's desired password. + @param completion Optionally; a block which is invoked when the sign up flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up + already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user + used, and prompt the user to sign in with one of those. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts + are not enabled. Enable them in the Auth section of the Firebase console. + + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo + dictionary object will contain more detailed explanation that can be shown to the user. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)createUserWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn confirmPasswordResetWithCode:newPassword:completion: + @brief Resets the password given a code sent to the user outside of the app and a new password + for the user. + + @param newPassword The new password. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + considered too weak. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled sign + in with the specified identity provider. + + `FIRAuthErrorCodeExpiredActionCode` - Indicates the OOB code is expired. + + `FIRAuthErrorCodeInvalidActionCode` - Indicates the OOB code is invalid. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)confirmPasswordResetWithCode:(NSString *)code + newPassword:(NSString *)newPassword + completion:(FIRConfirmPasswordResetCallback)completion; + +/** @fn checkActionCode:completion: + @brief Checks the validity of an out of band code. + + @param code The out of band code to check validity. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion; + +/** @fn verifyPasswordResetCode:completion: + @brief Checks the validity of a verify password reset code. + + @param code The password reset code to be verified. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)verifyPasswordResetCode:(NSString *)code + completion:(FIRVerifyPasswordResetCodeCallback)completion; + +/** @fn applyActionCode:completion: + @brief Applies out of band code. + + @param code The out of band code to be applied. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks This method will not work for out of band codes which require an additional parameter, + such as password reset code. + */ +- (void)applyActionCode:(NSString *)code + completion:(FIRApplyActionCodeCallback)completion; + +/** @fn sendPasswordResetWithEmail:completion: + @brief Initiates a password reset for the given email address. + + @param email The email address of the user. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + + */ +- (void)sendPasswordResetWithEmail:(NSString *)email + completion:(nullable FIRSendPasswordResetCallback)completion; + +/** @fn sendPasswordResetWithEmail:actionCodeSetting:completion: + @brief Initiates a password reset for the given email address and @FIRActionCodeSettings object. + + @param email The email address of the user. + @param actionCodeSettings An `FIRActionCodeSettings` object containing settings related to + handling action codes. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when + `handleCodeInApp` is set to YES. + + `FIRAuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name + is missing when the `androidInstallApp` flag is set to true. + + `FIRAuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the + continue URL is not whitelisted in the Firebase console. + + `FIRAuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the + continue URI is not valid. + + + */ + - (void)sendPasswordResetWithEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendPasswordResetCallback)completion; + +/** @fn sendSignInLinkToEmail:actionCodeSettings:completion: + @brief Sends a sign in with email link to provided email address. + + @param email The email address of the user. + @param actionCodeSettings An `FIRActionCodeSettings` object containing settings related to + handling action codes. + @param completion Optionally; a block which is invoked when the request finishes. Invoked + asynchronously on the main thread in the future. + */ +- (void)sendSignInLinkToEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendSignInLinkToEmailCallback)completion; + +/** @fn signOut: + @brief Signs out the current user. + + @param error Optionally; if an error occurs, upon return contains an NSError object that + describes the problem; is nil otherwise. + @return @YES when the sign out request was successful. @NO otherwise. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeKeychainError` - Indicates an error occurred when accessing the + keychain. The `NSLocalizedFailureReasonErrorKey` field in the `NSError.userInfo` + dictionary will contain more information about the error encountered. + + + + */ +- (BOOL)signOut:(NSError *_Nullable *_Nullable)error; + +/** @fn isSignInWithEmailLink + @brief Checks if link is an email sign-in link. + + @param link The email sign-in link. + @return @YES when the link passed matches the expected format of an email sign-in link. + */ +- (BOOL)isSignInWithEmailLink:(NSString *)link; + +/** @fn addAuthStateDidChangeListener: + @brief Registers a block as an "auth state did change" listener. To be invoked when: + + + The block is registered as a listener, + + A user with a different UID from the current user has signed in, or + + The current user has signed out. + + @param listener The block to be invoked. The block is always invoked asynchronously on the main + thread, even for it's initial invocation after having been added as a listener. + + @remarks The block is invoked immediately after adding it according to it's standard invocation + semantics, asynchronously on the main thread. Users should pay special attention to + making sure the block does not inadvertently retain objects which should not be retained by + the long-lived block. The block itself will be retained by `FIRAuth` until it is + unregistered or until the `FIRAuth` instance is otherwise deallocated. + + @return A handle useful for manually unregistering the block as a listener. + */ +- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener: + (FIRAuthStateDidChangeListenerBlock)listener; + +/** @fn removeAuthStateDidChangeListener: + @brief Unregisters a block as an "auth state did change" listener. + + @param listenerHandle The handle for the listener. + */ +- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle; + +/** @fn addIDTokenDidChangeListener: + @brief Registers a block as an "ID token did change" listener. To be invoked when: + + + The block is registered as a listener, + + A user with a different UID from the current user has signed in, + + The ID token of the current user has been refreshed, or + + The current user has signed out. + + @param listener The block to be invoked. The block is always invoked asynchronously on the main + thread, even for it's initial invocation after having been added as a listener. + + @remarks The block is invoked immediately after adding it according to it's standard invocation + semantics, asynchronously on the main thread. Users should pay special attention to + making sure the block does not inadvertently retain objects which should not be retained by + the long-lived block. The block itself will be retained by `FIRAuth` until it is + unregistered or until the `FIRAuth` instance is otherwise deallocated. + + @return A handle useful for manually unregistering the block as a listener. + */ +- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener: + (FIRIDTokenDidChangeListenerBlock)listener; + +/** @fn removeIDTokenDidChangeListener: + @brief Unregisters a block as an "ID token did change" listener. + + @param listenerHandle The handle for the listener. + */ +- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle; + +/** @fn useAppLanguage + @brief Sets `languageCode` to the app's current language. + */ +- (void)useAppLanguage; + +#if TARGET_OS_IOS + +/** @fn canHandleURL: + @brief Whether the specific URL is handled by `FIRAuth` . + @param URL The URL received by the application delegate from any of the openURL method. + @return Whether or the URL is handled. YES means the URL is for Firebase Auth + so the caller should ignore the URL from further processing, and NO means the + the URL is for the app (or another libaray) so the caller should continue handling + this URL as usual. + @remarks If swizzling is disabled, URLs received by the application delegate must be forwarded + to this method for phone number auth to work. + */ +- (BOOL)canHandleURL:(nonnull NSURL *)URL; + +/** @fn setAPNSToken:type: + @brief Sets the APNs token along with its type. + @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work, + by either setting calling this method or by setting the `APNSToken` property. + */ +- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type; + +/** @fn canHandleNotification: + @brief Whether the specific remote notification is handled by `FIRAuth` . + @param userInfo A dictionary that contains information related to the + notification in question. + @return Whether or the notification is handled. YES means the notification is for Firebase Auth + so the caller should ignore the notification from further processing, and NO means the + the notification is for the app (or another libaray) so the caller should continue handling + this notification as usual. + @remarks If swizzling is disabled, related remote notifications must be forwarded to this method + for phone number auth to work. + */ +- (BOOL)canHandleNotification:(NSDictionary *)userInfo; + +#endif // TARGET_OS_IOS + +#pragma mark - User sharing + +/** @fn useUserAccessGroup:error: + @brief Switch userAccessGroup and current user to the given accessGroup and the user stored in + it. + */ +- (BOOL)useUserAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError; + +/** @fn getStoredUserForAccessGroup:error: + @brief Get the stored user in the given accessGroup. + */ +- (nullable FIRUser *)getStoredUserForAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuth.mm b/auth/src/ios/fake/FIRAuth.mm new file mode 100644 index 0000000000..9c51dadae9 --- /dev/null +++ b/auth/src/ios/fake/FIRAuth.mm @@ -0,0 +1,214 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuth.h" +#import "auth/src/ios/fake/FIRAuthErrors.h" +#import "auth/src/ios/fake/FIRAuthDataResult.h" +#import "auth/src/ios/fake/FIRAuthUIDelegate.h" +#import "auth/src/ios/fake/FIRUser.h" + +#include +#include +#include "testing/util_ios.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const FIRAuthErrorUserInfoUpdatedCredentialKey = + @"FIRAuthErrorUserInfoUpdatedCredentialKey"; + +@implementation FIRAuth { + // Manages callbacks for testing. + firebase::testing::cppsdk::CallbackTickerManager _callbackManager; +} + +- (instancetype)init { + return [super init]; +} + ++ (FIRAuth *)auth { + return [[FIRAuth alloc] init]; +} + ++ (FIRAuth *)authWithApp:(FIRApp *)app { + FIRAuth *result = [[FIRAuth alloc] init]; + return result; +} + +static int AuthErrorFromConfig(const char *config_key) { + const firebase::testing::cppsdk::ConfigRow *row = + firebase::testing::cppsdk::ConfigGet(config_key); + if (row != nullptr && row->futuregeneric()->throwexception()) { + std::regex expression("^\\[.*[?!:]:?(.*)\\].*"); + std::smatch result; + std::string search_str(row->futuregeneric()->exceptionmsg()->c_str()); + if (std::regex_search(search_str, result, expression)) { + // The messages that throw errors should have: + // "[AndroidNamedException:ERROR_FIREBASE_PROBLEM] ". + // result.str(1) contains the "ERROR_FIREBASE_PROBLEM" part. + // The mapping between ios, android, and generic firebase errors is here: + // https://docs.google.com/spreadsheets/d/1U5ESSHoc10Vd7sDoQO-CbbQ46_ThGol2lhViFs8Eg2g/ + std::string error_code = result.str(1); + if (error_code == "ERROR_INVALID_CUSTOM_TOKEN") return FIRAuthErrorCodeInvalidCustomToken; + if (error_code == "ERROR_INVALID_EMAIL") return FIRAuthErrorCodeInvalidEmail; + if (error_code == "ERROR_OPERATION_NOT_ALLOWED") return FIRAuthErrorCodeOperationNotAllowed; + if (error_code == "ERROR_WRONG_PASSWORD") return FIRAuthErrorCodeWrongPassword; + if (error_code == "ERROR_EMAIL_ALREADY_IN_USE") return FIRAuthErrorCodeEmailAlreadyInUse; + if (error_code == "ERROR_INVALID_MESSAGE_PAYLOAD") + return FIRAuthErrorCodeInvalidMessagePayload; + } + } + return -1; +} + +- (void)updateCurrentUser:(FIRUser *)user completion:(nullable FIRUserUpdateCallback)completion {} + +- (void)fetchProvidersForEmail:(NSString *)email {} + +- (void)fetchProvidersForEmail:(NSString *)email + completion:(nullable FIRProviderQueryCallback)completion {} + +- (void)fetchSignInMethodsForEmail:(NSString *)email + completion:(nullable FIRSignInMethodQueryCallback)completion {} + +- (void)signInWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInWithEmail:password:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithEmail:password:completion:")); +} + +- (void)signInWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithCredential:completion:")); +} + +- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add( + @"FIRAuth.signInAndRetrieveDataWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInAndRetrieveDataWithCredential:completion:")); +} + +- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInAnonymouslyWithCompletion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInAnonymouslyWithCompletion:")); +} + +- (void)signInWithCustomToken:(NSString *)token + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.signInWithCustomToken:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithCustomToken:completion:")); +} + +- (void)signInWithEmail:(NSString *)email + link:(NSString *)link + completion:(nullable FIRAuthDataResultCallback)completion {} + +- (void)signInWithProvider:(id)provider + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthDataResultCallback)completion { + + _callbackManager.Add(@"FIRAuth.signInWithProvider:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.signInWithProvider:completion:")); +} + +- (void)createUserWithEmail:(NSString *)email + password:(NSString *)password + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRAuth.createUserWithEmail:password:completion:", completion, + [[FIRAuthDataResult alloc] init], + AuthErrorFromConfig("FIRAuth.createUserWithEmail:password:completion:")); +} + +- (void)confirmPasswordResetWithCode:(NSString *)code + newPassword:(NSString *)newPassword + completion:(FIRConfirmPasswordResetCallback)completion {} + +- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion {} + +- (void)verifyPasswordResetCode:(NSString *)code + completion:(FIRVerifyPasswordResetCodeCallback)completion {} + +- (void)applyActionCode:(NSString *)code + completion:(FIRApplyActionCodeCallback)completion {} + +- (void)sendPasswordResetWithEmail:(NSString *)email + completion:(nullable FIRSendPasswordResetCallback)completion { + _callbackManager.Add(@"FIRAuth.sendPasswordResetWithEmail:completion:", completion, + AuthErrorFromConfig("FIRAuth.sendPasswordResetWithEmail:completion:")); +} + +- (void)sendPasswordResetWithEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendPasswordResetCallback)completion {} + +- (void)sendSignInLinkToEmail:(NSString *)email + actionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendSignInLinkToEmailCallback)completion {} + +- (BOOL)signOut:(NSError *_Nullable *_Nullable)error { + return YES; +} + +- (BOOL)isSignInWithEmailLink:(NSString *)link { + return YES; +} + +- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener: + (FIRAuthStateDidChangeListenerBlock)listener { + return nil; +} + +- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle {} + +- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener: + (FIRIDTokenDidChangeListenerBlock)listener { + return nil; +} + +- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle {} + +- (void)useAppLanguage {} + +- (BOOL)canHandleURL:(nonnull NSURL *)URL { + return NO; +} + +- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type {} + +- (BOOL)canHandleNotification:(NSDictionary *)userInfo { + return NO; +} + +- (BOOL)useUserAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError { + return NO; +} + +- (nullable FIRUser *)getStoredUserForAccessGroup:(NSString *_Nullable)accessGroup + error:(NSError *_Nullable *_Nullable)outError { + return [[FIRUser alloc] init]; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthAPNSTokenType.h b/auth/src/ios/fake/FIRAuthAPNSTokenType.h new file mode 100644 index 0000000000..4f3c9f6a8a --- /dev/null +++ b/auth/src/ios/fake/FIRAuthAPNSTokenType.h @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * @brief The APNs token type for the app. + */ +typedef NS_ENUM(NSInteger, FIRAuthAPNSTokenType) { + + /** Unknown token type. + The actual token type will be detected from the provisioning profile in the app's bundle. + */ + FIRAuthAPNSTokenTypeUnknown, + + /** Sandbox token type. + */ + FIRAuthAPNSTokenTypeSandbox, + + /** Production token type. + */ + FIRAuthAPNSTokenTypeProd, +} NS_SWIFT_NAME(AuthAPNSTokenType); + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthCredential.h b/auth/src/ios/fake/FIRAuthCredential.h new file mode 100644 index 0000000000..c75d201454 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthCredential.h @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthCredential + @brief Represents a credential. + */ +NS_SWIFT_NAME(AuthCredential) +@interface FIRAuthCredential : NSObject + +/** @property provider + @brief Gets the name of the identity provider for the credential. + */ +@property(nonatomic, copy, readonly) NSString *provider; + +/** @fn init + @brief This is an abstract base class. Concrete instances should be created via factory + methods available in the various authentication provider libraries (like the Facebook + provider or the Google provider libraries.) + */ +- (instancetype)init NS_UNAVAILABLE; + +// Only used for testing. +- (instancetype)initWithProvider:(NSString *)provider NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthCredential.mm b/auth/src/ios/fake/FIRAuthCredential.mm new file mode 100644 index 0000000000..f1f603683e --- /dev/null +++ b/auth/src/ios/fake/FIRAuthCredential.mm @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAuthCredential + +- (instancetype)initWithProvider:(NSString *)provider { + self = [super init]; + if (self) { + _provider = [provider copy]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthDataResult.h b/auth/src/ios/fake/FIRAuthDataResult.h new file mode 100644 index 0000000000..42770d66e6 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthDataResult.h @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAdditionalUserInfo; +@class FIRAuthCredential; +@class FIRUser; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthDataResult + @brief Helper object that contains the result of a successful sign-in, link and reauthenticate + action. It contains references to a FIRUser instance and a FIRAdditionalUserInfo instance. + */ +NS_SWIFT_NAME(AuthDataResult) +@interface FIRAuthDataResult : NSObject + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be initialized manually. `FIRAuthDataResult` instance is + returned as part of `FIRAuthDataResultCallback`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @property user + @brief The signed in user. + */ +@property(nonatomic, readonly) FIRUser *user; + +/** @property additionalUserInfo + @brief If available contains the additional IdP specific information about signed in user. + */ +@property(nonatomic, readonly, nullable) FIRAdditionalUserInfo *additionalUserInfo; + +/** @property credential + @brief This property will be non-nil after a successful headful-lite sign-in via + signInWithProvider:UIDelegate:. May be used to obtain the accessToken and/or IDToken + pertaining to a recently signed-in user. + */ +@property(nonatomic, readonly, nullable) FIRAuthCredential *credential; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthDataResult.mm b/auth/src/ios/fake/FIRAuthDataResult.mm new file mode 100644 index 0000000000..fe7c9f81c4 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthDataResult.mm @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuthDataResult.h" + +#import "auth/src/ios/fake/FIRUser.h" +#import "auth/src/ios/fake/FIRAdditionalUserInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAuthDataResult + +- (instancetype)init { + self = [super init]; + if (self) { + _user = [[FIRUser alloc] init]; + _additionalUserInfo = [[FIRAdditionalUserInfo alloc] init]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthErrors.h b/auth/src/ios/fake/FIRAuthErrors.h new file mode 100644 index 0000000000..8874fb6111 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthErrors.h @@ -0,0 +1,358 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthErrors + @remarks Error Codes common to all API Methods: + + + `FIRAuthErrorCodeNetworkError` + + `FIRAuthErrorCodeUserNotFound` + + `FIRAuthErrorCodeUserTokenExpired` + + `FIRAuthErrorCodeTooManyRequests` + + `FIRAuthErrorCodeInvalidAPIKey` + + `FIRAuthErrorCodeAppNotAuthorized` + + `FIRAuthErrorCodeKeychainError` + + `FIRAuthErrorCodeInternalError` + + @remarks Common error codes for `FIRUser` operations: + + + `FIRAuthErrorCodeInvalidUserToken` + + `FIRAuthErrorCodeUserDisabled` + + */ +NS_SWIFT_NAME(AuthErrors) +@interface FIRAuthErrors + +/** + @brief The Firebase Auth error domain. + */ +extern NSString *const FIRAuthErrorDomain NS_SWIFT_NAME(AuthErrorDomain); + +/** + @brief The name of the key for the error short string of an error code. + */ +extern NSString *const FIRAuthErrorUserInfoNameKey NS_SWIFT_NAME(AuthErrorUserInfoNameKey); + +/** + @brief Errors with one of the following three codes: + - `FIRAuthErrorCodeAccountExistsWithDifferentCredential` + - `FIRAuthErrorCodeCredentialAlreadyInUse` + - `FIRAuthErrorCodeEmailAlreadyInUse` + may contain an `NSError.userInfo` dictinary object which contains this key. The value + associated with this key is an NSString of the email address of the account that already + exists. + */ +extern NSString *const FIRAuthErrorUserInfoEmailKey NS_SWIFT_NAME(AuthErrorUserInfoEmailKey); + +/** + @brief The key used to read the updated Auth credential from the userInfo dictionary of the + NSError object returned. This is the updated auth credential the developer should use for + recovery if applicable. + */ +extern NSString *const FIRAuthErrorUserInfoUpdatedCredentialKey + NS_SWIFT_NAME(AuthErrorUserInfoUpdatedCredentialKey); + +/** + @brief Error codes used by Firebase Auth. + */ +typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { + /** Indicates a validation error with the custom token. + */ + FIRAuthErrorCodeInvalidCustomToken = 17000, + + /** Indicates the service account and the API key belong to different projects. + */ + FIRAuthErrorCodeCustomTokenMismatch = 17002, + + /** Indicates the IDP token or requestUri is invalid. + */ + FIRAuthErrorCodeInvalidCredential = 17004, + + /** Indicates the user's account is disabled on the server. + */ + FIRAuthErrorCodeUserDisabled = 17005, + + /** Indicates the administrator disabled sign in with the specified identity provider. + */ + FIRAuthErrorCodeOperationNotAllowed = 17006, + + /** Indicates the email used to attempt a sign up is already in use. + */ + FIRAuthErrorCodeEmailAlreadyInUse = 17007, + + /** Indicates the email is invalid. + */ + FIRAuthErrorCodeInvalidEmail = 17008, + + /** Indicates the user attempted sign in with a wrong password. + */ + FIRAuthErrorCodeWrongPassword = 17009, + + /** Indicates that too many requests were made to a server method. + */ + FIRAuthErrorCodeTooManyRequests = 17010, + + /** Indicates the user account was not found. + */ + FIRAuthErrorCodeUserNotFound = 17011, + + /** Indicates account linking is required. + */ + FIRAuthErrorCodeAccountExistsWithDifferentCredential = 17012, + + /** Indicates the user has attemped to change email or password more than 5 minutes after + signing in. + */ + FIRAuthErrorCodeRequiresRecentLogin = 17014, + + /** Indicates an attempt to link a provider to which the account is already linked. + */ + FIRAuthErrorCodeProviderAlreadyLinked = 17015, + + /** Indicates an attempt to unlink a provider that is not linked. + */ + FIRAuthErrorCodeNoSuchProvider = 17016, + + /** Indicates user's saved auth credential is invalid, the user needs to sign in again. + */ + FIRAuthErrorCodeInvalidUserToken = 17017, + + /** Indicates a network error occurred (such as a timeout, interrupted connection, or + unreachable host). These types of errors are often recoverable with a retry. The + `NSUnderlyingError` field in the `NSError.userInfo` dictionary will contain the error + encountered. + */ + FIRAuthErrorCodeNetworkError = 17020, + + /** Indicates the saved token has expired, for example, the user may have changed account + password on another device. The user needs to sign in again on the device that made this + request. + */ + FIRAuthErrorCodeUserTokenExpired = 17021, + + /** Indicates an invalid API key was supplied in the request. + */ + FIRAuthErrorCodeInvalidAPIKey = 17023, + + /** Indicates that an attempt was made to reauthenticate with a user which is not the current + user. + */ + FIRAuthErrorCodeUserMismatch = 17024, + + /** Indicates an attempt to link with a credential that has already been linked with a + different Firebase account + */ + FIRAuthErrorCodeCredentialAlreadyInUse = 17025, + + /** Indicates an attempt to set a password that is considered too weak. + */ + FIRAuthErrorCodeWeakPassword = 17026, + + /** Indicates the App is not authorized to use Firebase Authentication with the + provided API Key. + */ + FIRAuthErrorCodeAppNotAuthorized = 17028, + + /** Indicates the OOB code is expired. + */ + FIRAuthErrorCodeExpiredActionCode = 17029, + + /** Indicates the OOB code is invalid. + */ + FIRAuthErrorCodeInvalidActionCode = 17030, + + /** Indicates that there are invalid parameters in the payload during a "send password reset + * email" attempt. + */ + FIRAuthErrorCodeInvalidMessagePayload = 17031, + + /** Indicates that the sender email is invalid during a "send password reset email" attempt. + */ + FIRAuthErrorCodeInvalidSender = 17032, + + /** Indicates that the recipient email is invalid. + */ + FIRAuthErrorCodeInvalidRecipientEmail = 17033, + + /** Indicates that an email address was expected but one was not provided. + */ + FIRAuthErrorCodeMissingEmail = 17034, + + // The enum values 17035 is reserved and should NOT be used for new error codes. + + /** Indicates that the iOS bundle ID is missing when a iOS App Store ID is provided. + */ + FIRAuthErrorCodeMissingIosBundleID = 17036, + + /** Indicates that the android package name is missing when the `androidInstallApp` flag is set + to true. + */ + FIRAuthErrorCodeMissingAndroidPackageName = 17037, + + /** Indicates that the domain specified in the continue URL is not whitelisted in the Firebase + console. + */ + FIRAuthErrorCodeUnauthorizedDomain = 17038, + + /** Indicates that the domain specified in the continue URI is not valid. + */ + FIRAuthErrorCodeInvalidContinueURI = 17039, + + /** Indicates that a continue URI was not provided in a request to the backend which requires + one. + */ + FIRAuthErrorCodeMissingContinueURI = 17040, + + /** Indicates that a phone number was not provided in a call to + `verifyPhoneNumber:completion:`. + */ + FIRAuthErrorCodeMissingPhoneNumber = 17041, + + /** Indicates that an invalid phone number was provided in a call to + `verifyPhoneNumber:completion:`. + */ + FIRAuthErrorCodeInvalidPhoneNumber = 17042, + + /** Indicates that the phone auth credential was created with an empty verification code. + */ + FIRAuthErrorCodeMissingVerificationCode = 17043, + + /** Indicates that an invalid verification code was used in the verifyPhoneNumber request. + */ + FIRAuthErrorCodeInvalidVerificationCode = 17044, + + /** Indicates that the phone auth credential was created with an empty verification ID. + */ + FIRAuthErrorCodeMissingVerificationID = 17045, + + /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request. + */ + FIRAuthErrorCodeInvalidVerificationID = 17046, + + /** Indicates that the APNS device token is missing in the verifyClient request. + */ + FIRAuthErrorCodeMissingAppCredential = 17047, + + /** Indicates that an invalid APNS device token was used in the verifyClient request. + */ + FIRAuthErrorCodeInvalidAppCredential = 17048, + + // The enum values between 17048 and 17051 are reserved and should NOT be used for new error + // codes. + + /** Indicates that the SMS code has expired. + */ + FIRAuthErrorCodeSessionExpired = 17051, + + /** Indicates that the quota of SMS messages for a given project has been exceeded. + */ + FIRAuthErrorCodeQuotaExceeded = 17052, + + /** Indicates that the APNs device token could not be obtained. The app may not have set up + remote notification correctly, or may fail to forward the APNs device token to FIRAuth + if app delegate swizzling is disabled. + */ + FIRAuthErrorCodeMissingAppToken = 17053, + + /** Indicates that the app fails to forward remote notification to FIRAuth. + */ + FIRAuthErrorCodeNotificationNotForwarded = 17054, + + /** Indicates that the app could not be verified by Firebase during phone number authentication. + */ + FIRAuthErrorCodeAppNotVerified = 17055, + + /** Indicates that the reCAPTCHA token is not valid. + */ + FIRAuthErrorCodeCaptchaCheckFailed = 17056, + + /** Indicates that an attempt was made to present a new web context while one was already being + presented. + */ + FIRAuthErrorCodeWebContextAlreadyPresented = 17057, + + /** Indicates that the URL presentation was cancelled prematurely by the user. + */ + FIRAuthErrorCodeWebContextCancelled = 17058, + + /** Indicates a general failure during the app verification flow. + */ + FIRAuthErrorCodeAppVerificationUserInteractionFailure = 17059, + + /** Indicates that the clientID used to invoke a web flow is invalid. + */ + FIRAuthErrorCodeInvalidClientID = 17060, + + /** Indicates that a network request within a SFSafariViewController or UIWebview failed. + */ + FIRAuthErrorCodeWebNetworkRequestFailed = 17061, + + /** Indicates that an internal error occurred within a SFSafariViewController or UIWebview. + */ + FIRAuthErrorCodeWebInternalError = 17062, + + /** Indicates a general failure during a web sign-in flow. + */ + FIRAuthErrorCodeWebSignInUserInteractionFailure = 17063, + + /** Indicates that the local player was not authenticated prior to attempting Game Center + signin. + */ + FIRAuthErrorCodeLocalPlayerNotAuthenticated = 17066, + + /** Indicates that a non-null user was expected as an argmument to the operation but a null + user was provided. + */ + FIRAuthErrorCodeNullUser = 17067, + + /** + * Represents the error code for when the given provider id for a web operation is invalid. + */ + FIRAuthErrorCodeInvalidProviderID = 17071, + + /** Indicates that the Firebase Dynamic Link domain used is either not configured or is + unauthorized for the current project. + */ + FIRAuthErrorCodeInvalidDynamicLinkDomain = 17074, + + /** Indicates that the GameKit framework is not linked prior to attempting Game Center signin. + */ + FIRAuthErrorCodeGameKitNotLinked = 17076, + + /** Indicates an error for when the client identifier is missing. + */ + FIRAuthErrorCodeMissingClientIdentifier = 17993, + + /** Indicates an error occurred while attempting to access the keychain. + */ + FIRAuthErrorCodeKeychainError = 17995, + + /** Indicates an internal error occurred. + */ + FIRAuthErrorCodeInternalError = 17999, + + /** Raised when a JWT fails to parse correctly. May be accompanied by an underlying error + describing which step of the JWT parsing process failed. + */ + FIRAuthErrorCodeMalformedJWT = 18000, +} NS_SWIFT_NAME(AuthErrorCode); + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthSettings.h b/auth/src/ios/fake/FIRAuthSettings.h new file mode 100644 index 0000000000..4ac7ce8762 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthSettings.h @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthSettings + @brief Determines settings related to an auth object. + */ +NS_SWIFT_NAME(AuthSettings) +@interface FIRAuthSettings : NSObject + +/** @property appVerificationDisabledForTesting + @brief Flag to determine whether app verification should be disabled for testing or not. + */ +@property(nonatomic, assign, getter=isAppVerificationDisabledForTesting) BOOL + appVerificationDisabledForTesting; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthTokenResult.h b/auth/src/ios/fake/FIRAuthTokenResult.h new file mode 100644 index 0000000000..515aa60d2c --- /dev/null +++ b/auth/src/ios/fake/FIRAuthTokenResult.h @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthTokenResult + @brief A data class containing the ID token JWT string and other properties associated with the + token including the decoded payload claims. + */ +NS_SWIFT_NAME(AuthTokenResult) +@interface FIRAuthTokenResult : NSObject + +/** @property token + @brief Stores the JWT string of the ID token. + */ +@property(nonatomic, readonly) NSString *token; + +/** @property expirationDate + @brief Stores the ID token's expiration date. + */ +@property(nonatomic, readonly) NSDate *expirationDate; + +/** @property authDate + @brief Stores the ID token's authentication date. + @remarks This is the date the user was signed in and NOT the date the token was refreshed. + */ +@property(nonatomic, readonly) NSDate *authDate; + +/** @property issuedAtDate + @brief Stores the date that the ID token was issued. + @remarks This is the date last refreshed and NOT the last authentication date. + */ +@property(nonatomic, readonly) NSDate *issuedAtDate; + +/** @property signInProvider + @brief Stores sign-in provider through which the token was obtained. + @remarks This does not necessarily map to provider IDs. + */ +@property(nonatomic, readonly) NSString *signInProvider; + +/** @property claims + @brief Stores the entire payload of claims found on the ID token. This includes the standard + reserved claims as well as custom claims set by the developer via the Admin SDK. + */ +@property(nonatomic, readonly) NSDictionary *claims; + + + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRAuthUIDelegate.h b/auth/src/ios/fake/FIRAuthUIDelegate.h new file mode 100644 index 0000000000..9df4f6e407 --- /dev/null +++ b/auth/src/ios/fake/FIRAuthUIDelegate.h @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class UIViewController; + +NS_ASSUME_NONNULL_BEGIN + +/** @protocol FIRAuthUIDelegate + @brief A protocol to handle user interface interactions for Firebase Auth. + */ +NS_SWIFT_NAME(AuthUIDelegate) +@protocol FIRAuthUIDelegate + +/** @fn presentViewController:animated:completion: + @brief If implemented, this method will be invoked when Firebase Auth needs to display a view + controller. + @param viewControllerToPresent The view controller to be presented. + @param flag Decides whether the view controller presentation should be animated or not. + @param completion The block to execute after the presentation finishes. This block has no return + value and takes no parameters. +*/ +- (void)presentViewController:(UIViewController *)viewControllerToPresent + animated:(BOOL)flag + completion:(void (^ _Nullable)(void))completion; + +/** @fn dismissViewControllerAnimated:completion: + @brief If implemented, this method will be invoked when Firebase Auth needs to display a view + controller. + @param flag Decides whether removing the view controller should be animated or not. + @param completion The block to execute after the presentation finishes. This block has no return + value and takes no parameters. +*/ +- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion + NS_SWIFT_NAME(dismiss(animated:completion:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIREmailAuthProvider.h b/auth/src/ios/fake/FIREmailAuthProvider.h new file mode 100644 index 0000000000..aac0bf0a0f --- /dev/null +++ b/auth/src/ios/fake/FIREmailAuthProvider.h @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the email & password identity provider. + */ +extern NSString *const FIREmailAuthProviderID NS_SWIFT_NAME(EmailAuthProviderID); + +/** + @brief A string constant identifying the email-link sign-in method. + */ +extern NSString *const FIREmailLinkAuthSignInMethod NS_SWIFT_NAME(EmailLinkAuthSignInMethod); + +/** + @brief A string constant identifying the email & password sign-in method. + */ +extern NSString *const FIREmailPasswordAuthSignInMethod + NS_SWIFT_NAME(EmailPasswordAuthSignInMethod); + +/** @class FIREmailAuthProvider + @brief A concrete implementation of `FIRAuthProvider` for Email & Password Sign In. + */ +NS_SWIFT_NAME(EmailAuthProvider) +@interface FIREmailAuthProvider : NSObject + +/** @fn credentialWithEmail:password: + @brief Creates an `FIRAuthCredential` for an email & password sign in. + + @param email The user's email address. + @param password The user's password. + @return A FIRAuthCredential containing the email & password credential. + */ ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password; + +/** @fn credentialWithEmail:Link: + @brief Creates an `FIRAuthCredential` for an email & link sign in. + + @param email The user's email address. + @param link The email sign-in link. + @return A FIRAuthCredential containing the email & link credential. + */ ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)link; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIREmailAuthProvider.mm b/auth/src/ios/fake/FIREmailAuthProvider.mm new file mode 100644 index 0000000000..52def576c0 --- /dev/null +++ b/auth/src/ios/fake/FIREmailAuthProvider.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIREmailAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIREmailAuthProvider + ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password { + return [[FIRAuthCredential alloc] initWithProvider:@"password"]; +} + ++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)link { + return [[FIRAuthCredential alloc] initWithProvider:@"link"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRFacebookAuthProvider.h b/auth/src/ios/fake/FIRFacebookAuthProvider.h new file mode 100644 index 0000000000..75efe13f4a --- /dev/null +++ b/auth/src/ios/fake/FIRFacebookAuthProvider.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Facebook identity provider. + */ +extern NSString *const FIRFacebookAuthProviderID NS_SWIFT_NAME(FacebookAuthProviderID); + +/** + @brief A string constant identifying the Facebook sign-in method. + */ +extern NSString *const _Nonnull FIRFacebookAuthSignInMethod NS_SWIFT_NAME(FacebookAuthSignInMethod); + +/** @class FIRFacebookAuthProvider + @brief Utility class for constructing Facebook credentials. + */ +NS_SWIFT_NAME(FacebookAuthProvider) +@interface FIRFacebookAuthProvider : NSObject + +/** @fn credentialWithAccessToken: + @brief Creates an `FIRAuthCredential` for a Facebook sign in. + + @param accessToken The Access Token from Facebook. + @return A FIRAuthCredential containing the Facebook credentials. + */ ++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken; + +/** @fn init + @brief This class should not be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRFacebookAuthProvider.mm b/auth/src/ios/fake/FIRFacebookAuthProvider.mm new file mode 100644 index 0000000000..21c7e39ebe --- /dev/null +++ b/auth/src/ios/fake/FIRFacebookAuthProvider.mm @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRAuthCredential.h" +#import "auth/src/ios/fake/FIRFacebookAuthProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRFacebookAuthProvider + ++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken { + return [[FIRAuthCredential alloc] initWithProvider:@"facebook.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRFederatedAuthProvider.h b/auth/src/ios/fake/FIRFederatedAuthProvider.h new file mode 100644 index 0000000000..51190e28cd --- /dev/null +++ b/auth/src/ios/fake/FIRFederatedAuthProvider.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#if TARGET_OS_IOS +#import "FIRAuthUIDelegate.h" +#endif // TARGET_OS_IOS + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(FederatedAuthProvider) +@protocol FIRFederatedAuthProvider + +/** @typedef FIRAuthCredentialCallback + @brief The type of block invoked when obtaining an auth credential. + @param credential The credential obtained. + @param error The error that occurred if any. + */ +typedef void(^FIRAuthCredentialCallback)(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthCredentialCallback); + +#if TARGET_OS_IOS +/** @fn getCredentialWithUIDelegate:completion: + @brief Used to obtain an auth credential via a mobile web flow. + @param UIDelegate An optional UI delegate used to presenet the mobile web flow. + @param completion Optionally; a block which is invoked asynchronously on the main thread when + the mobile web flow is completed. + */ +- (void)getCredentialWithUIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthCredentialCallback)completion; +#endif // TARGET_OS_IOS + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGameCenterAuthProvider.h b/auth/src/ios/fake/FIRGameCenterAuthProvider.h new file mode 100644 index 0000000000..5e59404ada --- /dev/null +++ b/auth/src/ios/fake/FIRGameCenterAuthProvider.h @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Game Center identity provider. + */ +extern NSString *const FIRGameCenterAuthProviderID NS_SWIFT_NAME(GameCenterAuthProviderID); + +/** + @brief A string constant identifying the Game Center sign-in method. + */ +extern NSString *const _Nonnull FIRGameCenterAuthSignInMethod +NS_SWIFT_NAME(GameCenterAuthSignInMethod); + +/** @typedef FIRGameCenterCredentialCallback + @brief The type of block invoked when the Game Center credential code has finished. + @param credential On success, the credential will be provided, nil otherwise. + @param error On error, the error that occurred, nil otherwise. + */ +typedef void (^FIRGameCenterCredentialCallback)(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) +NS_SWIFT_NAME(GameCenterCredentialCallback); + +/** @class FIRGameCenterAuthProvider + @brief A concrete implementation of @c FIRAuthProvider for Game Center Sign In. + */ +NS_SWIFT_NAME(GameCenterAuthProvider) +@interface FIRGameCenterAuthProvider : NSObject + +/** @fn getCredentialWithCompletion: + @brief Creates a @c FIRAuthCredential for a Game Center sign in. + */ ++ (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion +NS_SWIFT_NAME(getCredential(completion:)); + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGameCenterAuthProvider.mm b/auth/src/ios/fake/FIRGameCenterAuthProvider.mm new file mode 100644 index 0000000000..854b925900 --- /dev/null +++ b/auth/src/ios/fake/FIRGameCenterAuthProvider.mm @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRGameCenterAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +@implementation FIRGameCenterAuthProvider + ++ (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion { + completion([[FIRAuthCredential alloc] initWithProvider:@"gc.apple.com"], nil); +} + +@end diff --git a/auth/src/ios/fake/FIRGitHubAuthProvider.h b/auth/src/ios/fake/FIRGitHubAuthProvider.h new file mode 100644 index 0000000000..0610427a44 --- /dev/null +++ b/auth/src/ios/fake/FIRGitHubAuthProvider.h @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the GitHub identity provider. + */ +extern NSString *const FIRGitHubAuthProviderID NS_SWIFT_NAME(GitHubAuthProviderID); + +/** + @brief A string constant identifying the GitHub sign-in method. + */ +extern NSString *const _Nonnull FIRGitHubAuthSignInMethod NS_SWIFT_NAME(GitHubAuthSignInMethod); + + +/** @class FIRGitHubAuthProvider + @brief Utility class for constructing GitHub credentials. + */ +NS_SWIFT_NAME(GitHubAuthProvider) +@interface FIRGitHubAuthProvider : NSObject + +/** @fn credentialWithToken: + @brief Creates an `FIRAuthCredential` for a GitHub sign in. + + @param token The GitHub OAuth access token. + @return A FIRAuthCredential containing the GitHub credential. + */ ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGitHubAuthProvider.mm b/auth/src/ios/fake/FIRGitHubAuthProvider.mm new file mode 100644 index 0000000000..6abb95e40e --- /dev/null +++ b/auth/src/ios/fake/FIRGitHubAuthProvider.mm @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRGitHubAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGitHubAuthProvider + ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token { + return [[FIRAuthCredential alloc] initWithProvider:@"github.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGoogleAuthProvider.h b/auth/src/ios/fake/FIRGoogleAuthProvider.h new file mode 100644 index 0000000000..7d6fa226e5 --- /dev/null +++ b/auth/src/ios/fake/FIRGoogleAuthProvider.h @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Google identity provider. + */ +extern NSString *const FIRGoogleAuthProviderID NS_SWIFT_NAME(GoogleAuthProviderID); + +/** + @brief A string constant identifying the Google sign-in method. + */ +extern NSString *const _Nonnull FIRGoogleAuthSignInMethod NS_SWIFT_NAME(GoogleAuthSignInMethod); + +/** @class FIRGoogleAuthProvider + @brief Utility class for constructing Google Sign In credentials. + */ +NS_SWIFT_NAME(GoogleAuthProvider) +@interface FIRGoogleAuthProvider : NSObject + +/** @fn credentialWithIDToken:accessToken: + @brief Creates an `FIRAuthCredential` for a Google sign in. + + @param IDToken The ID Token from Google. + @param accessToken The Access Token from Google. + @return A FIRAuthCredential containing the Google credentials. + */ ++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken + accessToken:(NSString *)accessToken; + +/** @fn init + @brief This class should not be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRGoogleAuthProvider.mm b/auth/src/ios/fake/FIRGoogleAuthProvider.mm new file mode 100644 index 0000000000..7e374c063e --- /dev/null +++ b/auth/src/ios/fake/FIRGoogleAuthProvider.mm @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRGoogleAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGoogleAuthProvider + ++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken + accessToken:(NSString *)accessToken { + return [[FIRAuthCredential alloc] initWithProvider:@"google.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthCredential.h b/auth/src/ios/fake/FIROAuthCredential.h new file mode 100644 index 0000000000..5e54198140 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthCredential.h @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIROAuthCredential + @brief Internal implementation of FIRAuthCredential for generic credentials. + */ +NS_SWIFT_NAME(OAuthCredential) +@interface FIROAuthCredential : FIRAuthCredential + +/** @property IDToken + @brief The ID Token associated with this credential. + */ +@property(nonatomic, readonly, nullable) NSString *IDToken; + +/** @property accessToken + @brief The access token associated with this credential. + */ +@property(nonatomic, readonly, nullable) NSString *accessToken; + +/** @property secret + @brief The secret associated with this credential. This will be nil for OAuth 2.0 providers. + @detail OAuthCredential already exposes a providerId getter. This will help the developer + determine whether an access token/secret pair is needed. + */ +@property(nonatomic, readonly, nullable) NSString *secret; + +#if !defined(FIREBASE_AUTH_TESTING) +/** @fn init + @brief This class is not supposed to be instantiated directly. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // !defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthCredential.mm b/auth/src/ios/fake/FIROAuthCredential.mm new file mode 100644 index 0000000000..81852363b8 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthCredential.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIROAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIROAuthCredential + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder {} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithProvider:@"oauth"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthProvider.h b/auth/src/ios/fake/FIROAuthProvider.h new file mode 100644 index 0000000000..a46eb2ccf0 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthProvider.h @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRFederatedAuthProvider.h" + +@class FIRAuth; +@class FIROAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIROAuthProvider + @brief A concrete implementation of `FIRAuthProvider` for generic OAuth Providers. + */ +NS_SWIFT_NAME(OAuthProvider) +@interface FIROAuthProvider : NSObject + +/** @property scopes + @brief Array used to configure the OAuth scopes. + */ +@property(nonatomic, copy, nullable) NSArray *scopes; + +/** @property customParameters + @brief Dictionary used to configure the OAuth custom parameters. + */ +@property(nonatomic, copy, nullable) NSDictionary *customParameters; + +/** @property providerID + @brief The provider ID indicating the specific OAuth provider this OAuthProvider instance + represents. + */ +@property(nonatomic, copy, readonly) NSString *providerID; + +/** @fn providerWithProviderID: + @param providerID The provider ID of the IDP for which this auth provider instance will be + configured. + @return An instance of FIROAuthProvider corresponding to the specified provider ID. + */ ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID; + +/** @fn providerWithProviderID:auth: + @param providerID The provider ID of the IDP for which this auth provider instance will be + configured. + @param auth The auth instance to be associated with the FIROAuthProvider instance. + @return An instance of FIROAuthProvider corresponding to the specified provider ID. + */ ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID auth:(FIRAuth *)auth; + +/** @fn credentialWithProviderID:IDToken:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID, ID + token and access token. + + @param providerID The provider ID associated with the Auth credential being created. + @param IDToken The IDToken associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created, if + available. + @return A FIRAuthCredential for the specified provider ID, ID token and access token. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken; + +/** @fn credentialWithProviderID:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID using + an ID token. + + @param providerID The provider ID associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created + @return A FIRAuthCredential. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + accessToken:(NSString *)accessToken; + +/** @fn credentialWithProviderID:IDToken:rawNonce:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID, ID + token, raw nonce and access token. + @param providerID The provider ID associated with the Auth credential being created. + @param IDToken The IDToken associated with the Auth credential being created. + @param rawNonce The raw nonce associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created, if + available. + @return A FIRAuthCredential for the specified provider ID, ID token and access token. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + rawNonce:(nullable NSString *)rawNonce + accessToken:(nullable NSString *)accessToken; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +// Exposed for testing. +- (instancetype)initWithProviderID:(NSString *)providerID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIROAuthProvider.mm b/auth/src/ios/fake/FIROAuthProvider.mm new file mode 100644 index 0000000000..44862f8089 --- /dev/null +++ b/auth/src/ios/fake/FIROAuthProvider.mm @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIROAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" +#import "auth/src/ios/fake/FIROAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIROAuthProvider + +- (void)getCredentialWithUIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthCredentialCallback)completion {} + ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID { + return [[FIROAuthProvider alloc] initWithProviderID:providerID]; +} + ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID + auth:(FIRAuth *)auth { + return [[FIROAuthProvider alloc] initWithProviderID:providerID]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken { + return [[FIROAuthCredential alloc] initWithProvider:providerID]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + accessToken:(NSString *)accessToken { + return [[FIROAuthCredential alloc] initWithProvider:providerID]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + rawNonce:(nullable NSString *)rawNonce + accessToken:(nullable NSString *)accessToken { + return [[FIROAuthCredential alloc] initWithProvider:providerID]; +} + +#pragma mark - Internal Methods + +- (instancetype)initWithProviderID:(NSString *)providerID { + self = [super init]; + if (self) { + _providerID = providerID; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthCredential.h b/auth/src/ios/fake/FIRPhoneAuthCredential.h new file mode 100644 index 0000000000..8badab6a25 --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthCredential.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRPhoneAuthCredential + @brief Implementation of FIRAuthCredential for Phone Auth credentials. + */ +NS_SWIFT_NAME(PhoneAuthCredential) +@interface FIRPhoneAuthCredential : FIRAuthCredential + +#if !defined(FIREBASE_AUTH_TESTING) +/** @fn init + @brief This class is not supposed to be instantiated directly. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // !defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthCredential.mm b/auth/src/ios/fake/FIRPhoneAuthCredential.mm new file mode 100644 index 0000000000..e64933fead --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthCredential.mm @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRPhoneAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRPhoneAuthCredential + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder {} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithProvider:@"phone"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthProvider.h b/auth/src/ios/fake/FIRPhoneAuthProvider.h new file mode 100644 index 0000000000..805d0065f2 --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthProvider.h @@ -0,0 +1,109 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuth; +@class FIRPhoneAuthCredential; +@protocol FIRAuthUIDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** @var FIRPhoneAuthProviderID + @brief A string constant identifying the phone identity provider. + */ +extern NSString *const FIRPhoneAuthProviderID NS_SWIFT_NAME(PhoneAuthProviderID); + +/** @var FIRPhoneAuthProviderID + @brief A string constant identifying the phone sign-in method. + */ +extern NSString *const _Nonnull FIRPhoneAuthSignInMethod NS_SWIFT_NAME(PhoneAuthSignInMethod); + +/** @typedef FIRVerificationResultCallback + @brief The type of block invoked when a request to send a verification code has finished. + + @param verificationID On success, the verification ID provided, nil otherwise. + @param error On error, the error that occurred, nil otherwise. + */ +typedef void (^FIRVerificationResultCallback) + (NSString *_Nullable verificationID, NSError *_Nullable error) + NS_SWIFT_NAME(VerificationResultCallback); + +/** @class FIRPhoneAuthProvider + @brief A concrete implementation of `FIRAuthProvider` for phone auth providers. + */ +NS_SWIFT_NAME(PhoneAuthProvider) +@interface FIRPhoneAuthProvider : NSObject + +/** @fn provider + @brief Returns an instance of `FIRPhoneAuthProvider` for the default `FIRAuth` object. + */ ++ (instancetype)provider NS_SWIFT_NAME(provider()); + +/** @fn providerWithAuth: + @brief Returns an instance of `FIRPhoneAuthProvider` for the provided `FIRAuth` object. + + @param auth The auth object to associate with the phone auth provider instance. + */ ++ (instancetype)providerWithAuth:(FIRAuth *)auth NS_SWIFT_NAME(provider(auth:)); + +/** @fn verifyPhoneNumber:UIDelegate:completion: + @brief Starts the phone number authentication flow by sending a verification code to the + specified phone number. + @param phoneNumber The phone number to be verified. + @param UIDelegate An object used to present the SFSafariViewController. The object is retained + by this method until the completion block is executed. + @param completion The callback to be invoked when the verification flow is finished. + @remarks Possible error codes: + + + `FIRAuthErrorCodeCaptchaCheckFailed` - Indicates that the reCAPTCHA token obtained by + the Firebase Auth is invalid or has expired. + + `FIRAuthErrorCodeQuotaExceeded` - Indicates that the phone verification quota for this + project has been exceeded. + + `FIRAuthErrorCodeInvalidPhoneNumber` - Indicates that the phone number provided is + invalid. + + `FIRAuthErrorCodeMissingPhoneNumber` - Indicates that a phone number was not provided. + */ +- (void)verifyPhoneNumber:(NSString *)phoneNumber + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRVerificationResultCallback)completion; + +/** @fn credentialWithVerificationID:verificationCode: + @brief Creates an `FIRAuthCredential` for the phone number provider identified by the + verification ID and verification code. + + @param verificationID The verification ID obtained from invoking + verifyPhoneNumber:completion: + @param verificationCode The verification code obtained from the user. + @return The corresponding phone auth credential for the verification ID and verification code + provided. + */ +- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID + verificationCode:(NSString *)verificationCode; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief Please use the `provider` or `providerWithAuth:` methods to obtain an instance of + `FIRPhoneAuthProvider`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRPhoneAuthProvider.mm b/auth/src/ios/fake/FIRPhoneAuthProvider.mm new file mode 100644 index 0000000000..2e1d735241 --- /dev/null +++ b/auth/src/ios/fake/FIRPhoneAuthProvider.mm @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRPhoneAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" +#import "auth/src/ios/fake/FIRPhoneAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRPhoneAuthProvider + +- (instancetype)init { + return [super init]; +} + ++ (instancetype)provider { + return [[FIRPhoneAuthProvider alloc] init]; +} + ++ (instancetype)providerWithAuth:(FIRAuth *)auth { + return [FIRPhoneAuthProvider provider]; +} + +- (void)verifyPhoneNumber:(NSString *)phoneNumber + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRVerificationResultCallback)completion {} + +- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID + verificationCode:(NSString *)verificationCode { + return [[FIRPhoneAuthCredential alloc] initWithProvider:@"phone"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRTwitterAuthProvider.h b/auth/src/ios/fake/FIRTwitterAuthProvider.h new file mode 100644 index 0000000000..0f1b28d737 --- /dev/null +++ b/auth/src/ios/fake/FIRTwitterAuthProvider.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief A string constant identifying the Twitter identity provider. + */ +extern NSString *const FIRTwitterAuthProviderID NS_SWIFT_NAME(TwitterAuthProviderID); +/** + @brief A string constant identifying the Twitter sign-in method. + */ +extern NSString *const _Nonnull FIRTwitterAuthSignInMethod NS_SWIFT_NAME(TwitterAuthSignInMethod); + +/** @class FIRTwitterAuthProvider + @brief Utility class for constructing Twitter credentials. + */ +NS_SWIFT_NAME(TwitterAuthProvider) +@interface FIRTwitterAuthProvider : NSObject + +/** @fn credentialWithToken:secret: + @brief Creates an `FIRAuthCredential` for a Twitter sign in. + + @param token The Twitter OAuth token. + @param secret The Twitter OAuth secret. + @return A FIRAuthCredential containing the Twitter credential. + */ ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret; + +/** @fn init + @brief This class is not meant to be initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRTwitterAuthProvider.mm b/auth/src/ios/fake/FIRTwitterAuthProvider.mm new file mode 100644 index 0000000000..515573e68d --- /dev/null +++ b/auth/src/ios/fake/FIRTwitterAuthProvider.mm @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRTwitterAuthProvider.h" + +#import "auth/src/ios/fake/FIRAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRTwitterAuthProvider + ++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret { + return [[FIRAuthCredential alloc] initWithProvider:@"twitter.com"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUser.h b/auth/src/ios/fake/FIRUser.h new file mode 100644 index 0000000000..f65108749b --- /dev/null +++ b/auth/src/ios/fake/FIRUser.h @@ -0,0 +1,507 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRAuth.h" +#import "FIRAuthDataResult.h" +#import "FIRUserInfo.h" + +@class FIRAuthTokenResult; +@class FIRPhoneAuthCredential; +@class FIRUserProfileChangeRequest; +@class FIRUserMetadata; + +namespace firebase { +namespace testing { +namespace cppsdk { +class CallbackTickerManager; +} // namespace cppsdk +} // namespace testing +} // namespace firebase + +NS_ASSUME_NONNULL_BEGIN + +/** @typedef FIRAuthTokenCallback + @brief The type of block called when a token is ready for use. + @see FIRUser.getIDTokenWithCompletion: + @see FIRUser.getIDTokenForcingRefresh:withCompletion: + + @param token Optionally; an access token if the request was successful. + @param error Optionally; the error which occurred - or nil if the request was successful. + + @remarks One of: `token` or `error` will always be non-nil. + */ +typedef void (^FIRAuthTokenCallback)(NSString *_Nullable token, NSError *_Nullable error) + NS_SWIFT_NAME(AuthTokenCallback); + +/** @typedef FIRAuthTokenResultCallback + @brief The type of block called when a token is ready for use. + @see FIRUser.getIDTokenResultWithCompletion: + @see FIRUser.getIDTokenResultForcingRefresh:withCompletion: + + @param tokenResult Optionally; an object containing the raw access token string as well as other + useful data pertaining to the token. + @param error Optionally; the error which occurred - or nil if the request was successful. + + @remarks One of: `token` or `error` will always be non-nil. + */ +typedef void (^FIRAuthTokenResultCallback)(FIRAuthTokenResult *_Nullable tokenResult, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthTokenResultCallback); + +/** @typedef FIRUserProfileChangeCallback + @brief The type of block called when a user profile change has finished. + + @param error Optionally; the error which occurred - or nil if the request was successful. + */ +typedef void (^FIRUserProfileChangeCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(UserProfileChangeCallback); + +/** @typedef FIRSendEmailVerificationCallback + @brief The type of block called when a request to send an email verification has finished. + + @param error Optionally; the error which occurred - or nil if the request was successful. + */ +typedef void (^FIRSendEmailVerificationCallback)(NSError *_Nullable error) + NS_SWIFT_NAME(SendEmailVerificationCallback); + +/** @class FIRUser + @brief Represents a user. Firebase Auth does not attempt to validate users + when loading them from the keychain. Invalidated users (such as those + whose passwords have been changed on another client) are automatically + logged out when an auth-dependent operation is attempted or when the + ID token is automatically refreshed. + @remarks This class is thread-safe. + */ +NS_SWIFT_NAME(User) +@interface FIRUser : NSObject + +/** @property anonymous + @brief Indicates the user represents an anonymous user. + */ +@property(nonatomic, readonly, getter=isAnonymous) BOOL anonymous; + +/** @property emailVerified + @brief Indicates the email address associated with this user has been verified. + */ +@property(nonatomic, readonly, getter=isEmailVerified) BOOL emailVerified; + +/** @property refreshToken + @brief A refresh token; useful for obtaining new access tokens independently. + @remarks This property should only be used for advanced scenarios, and is not typically needed. + */ +@property(nonatomic, readonly, nullable) NSString *refreshToken; + +/** @property providerData + @brief Profile data for each identity provider, if any. + @remarks This data is cached on sign-in and updated when linking or unlinking. + */ +@property(nonatomic, readonly, nonnull) NSArray> *providerData; + +/** @property metadata + @brief Metadata associated with the Firebase user in question. + */ +@property(nonatomic, readonly, nonnull) FIRUserMetadata *metadata; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be instantiated. + @remarks To retrieve the current user, use `FIRAuth.currentUser`. To sign a user + in or out, use the methods on `FIRAuth`. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +/** @fn updateEmail:completion: + @brief Updates the email address for the user. On success, the cached user profile data is + updated. + @remarks May fail if there is already an account with this email address that was created using + email and password authentication. + + @param email The email address for the user. + @param completion Optionally; the block invoked when the user profile change has finished. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another + account. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security + sensitive operation that requires a recent login from the user. This error indicates + the user has not signed in recently enough. To resolve, reauthenticate the user by + invoking reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion + NS_SWIFT_NAME(updateEmail(to:completion:)); + +/** @fn updatePassword:completion: + @brief Updates the password for the user. On success, the cached user profile data is updated. + + @param password The new password for the user. + @param completion Optionally; the block invoked when the user profile change has finished. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled + sign in with the specified identity provider. + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security + sensitive operation that requires a recent login from the user. This error indicates + the user has not signed in recently enough. To resolve, reauthenticate the user by + invoking reauthenticateWithCredential:completion: on FIRUser. + + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo + dictionary object will contain more detailed explanation that can be shown to the user. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)updatePassword:(NSString *)password + completion:(nullable FIRUserProfileChangeCallback)completion + NS_SWIFT_NAME(updatePassword(to:completion:)); + +#if TARGET_OS_IOS +/** @fn updatePhoneNumberCredential:completion: + @brief Updates the phone number for the user. On success, the cached user profile data is + updated. + + @param phoneNumberCredential The new phone number credential corresponding to the phone number + to be added to the Firebase account, if a phone number is already linked to the account this + new phone number will replace it. + @param completion Optionally; the block invoked when the user profile change has finished. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security + sensitive operation that requires a recent login from the user. This error indicates + the user has not signed in recently enough. To resolve, reauthenticate the user by + invoking reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneNumberCredential + completion:(nullable FIRUserProfileChangeCallback)completion; +#endif + +/** @fn profileChangeRequest + @brief Creates an object which may be used to change the user's profile data. + + @remarks Set the properties of the returned object, then call + `FIRUserProfileChangeRequest.commitChangesWithCallback:` to perform the updates atomically. + + @return An object which may be used to change the user's profile data atomically. + */ +- (FIRUserProfileChangeRequest *)profileChangeRequest NS_SWIFT_NAME(createProfileChangeRequest()); + +/** @fn reloadWithCompletion: + @brief Reloads the user's profile data from the server. + + @param completion Optionally; the block invoked when the reload has finished. Invoked + asynchronously on the main thread in the future. + + @remarks May fail with a `FIRAuthErrorCodeRequiresRecentLogin` error code. In this case + you should call `FIRUser.reauthenticateWithCredential:completion:` before re-invoking + `FIRUser.updateEmail:completion:`. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion; + +/** @fn reauthenticateWithCredential:completion: + @brief Renews the user's authentication tokens by validating a fresh set of credentials supplied + by the user and returns additional identity provider data. + + @param credential A user-supplied credential, which will be validated by the server. This can be + a successful third-party identity provider sign-in, or an email address and password. + @param completion Optionally; the block invoked when the re-authentication operation has + finished. Invoked asynchronously on the main thread in the future. + + @remarks If the user associated with the supplied credential is different from the current user, + or if the validation of the supplied credentials fails; an error is returned and the current + user remains signed in. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + This could happen if it has expired or it is malformed. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that accounts with the + identity provider represented by the credential are not enabled. Enable them in the + Auth section of the Firebase console. + + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential + (e.g. the email in a Facebook access token) is already in use by an existing account, + that cannot be authenticated with this method. Call fetchProvidersForEmail for + this user’s email and then prompt them to sign in with any of the sign-in providers + returned. This error will only be thrown if the "One account per email address" + setting is enabled in the Firebase console, under Auth settings. Please note that the + error code raised in this specific situation may not be the same on Web and Android. + + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with + an incorrect password, if credential is of the type EmailPasswordAuthCredential. + + `FIRAuthErrorCodeUserMismatch` - Indicates that an attempt was made to + reauthenticate with a user which is not the current user. + + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn reauthenticateAndRetrieveDataWithCredential:completion: + @brief Please use linkWithCredential:completion: for Objective-C + or link(withCredential:completion:) for Swift instead. + */ +- (void)reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion +DEPRECATED_MSG_ATTRIBUTE( "Please use reauthenticateWithCredential:completion: for" + " Objective-C or reauthenticate(withCredential:completion:)" + " for Swift instead."); + +/** @fn getIDTokenResultWithCompletion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenResultWithCompletion:(nullable FIRAuthTokenResultCallback)completion + NS_SWIFT_NAME(getIDTokenResult(completion:)); + +/** @fn getIDTokenResultForcingRefresh:completion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason + other than an expiration. + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks The authentication token will be refreshed (by making a network request) if it has + expired, or if `forceRefresh` is YES. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenResultForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenResultCallback)completion + NS_SWIFT_NAME(getIDTokenResult(forcingRefresh:completion:)); + +/** @fn getIDTokenWithCompletion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion + NS_SWIFT_NAME(getIDToken(completion:)); + +/** @fn getIDTokenForcingRefresh:completion: + @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + + @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason + other than an expiration. + @param completion Optionally; the block invoked when the token is available. Invoked + asynchronously on the main thread in the future. + + @remarks The authentication token will be refreshed (by making a network request) if it has + expired, or if `forceRefresh` is YES. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. + */ +- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenCallback)completion; + +/** @fn linkAndRetrieveDataWithCredential:completion: + @brief Please use linkWithCredential:completion: for Objective-C + or link(withCredential:completion:) for Swift instead. + */ +- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion +DEPRECATED_MSG_ATTRIBUTE("Please use linkWithCredential:completion: for Objective-C " + "or link(withCredential:completion:) for Swift instead."); + +/** @fn linkWithCredential:completion: + @brief Associates a user account from a third-party identity provider with this user and + returns additional identity provider data. + + @param credential The credential for the identity provider. + @param completion Optionally; the block invoked when the unlinking is complete, or fails. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a + type already linked to this account. + + `FIRAuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a + credential that has already been linked with a different Firebase account. + + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity + provider represented by the credential are not enabled. Enable them in the Auth section + of the Firebase console. + + @remarks This method may also return error codes associated with updateEmail:completion: and + updatePassword:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)linkWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion; + +/** @fn unlinkFromProvider:completion: + @brief Disassociates a user account from a third-party identity provider with this user. + + @param provider The provider ID of the provider to unlink. + @param completion Optionally; the block invoked when the unlinking is complete, or fails. + Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider + that is not linked to the account. + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + operation that requires a recent login from the user. This error indicates the user + has not signed in recently enough. To resolve, reauthenticate the user by invoking + reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)unlinkFromProvider:(NSString *)provider + completion:(nullable FIRAuthResultCallback)completion; + +/** @fn sendEmailVerificationWithCompletion: + @brief Initiates email verification for the user. + + @param completion Optionally; the block invoked when the request to send an email verification + is complete, or fails. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeUserNotFound` - Indicates the user account was not found. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + */ +- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion; + +/** @fn sendEmailVerificationWithActionCodeSettings:completion: + @brief Initiates email verification for the user. + + @param actionCodeSettings An `FIRActionCodeSettings` object containing settings related to + handling action codes. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + sent in the request. + + `FIRAuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + the console for this action. + + `FIRAuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + sending update email. + + `FIRAuthErrorCodeUserNotFound` - Indicates the user account was not found. + + `FIRAuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when + a iOS App Store ID is provided. + + `FIRAuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name + is missing when the `androidInstallApp` flag is set to true. + + `FIRAuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the + continue URL is not whitelisted in the Firebase console. + + `FIRAuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the + continue URI is not valid. + */ +- (void)sendEmailVerificationWithActionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendEmailVerificationCallback) + completion; + +/** @fn deleteWithCompletion: + @brief Deletes the user account (also signs out the user, if this was the current user). + + @param completion Optionally; the block invoked when the request to delete the account is + complete, or fails. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: + + + `FIRAuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + operation that requires a recent login from the user. This error indicates the user + has not signed in recently enough. To resolve, reauthenticate the user by invoking + reauthenticateWithCredential:completion: on FIRUser. + + @remarks See `FIRAuthErrors` for a list of error codes that are common to all FIRUser methods. + + */ +- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion; + +@end + +/** @class FIRUserProfileChangeRequest + @brief Represents an object capable of updating a user's profile data. + @remarks Properties are marked as being part of a profile update when they are set. Setting a + property value to nil is not the same as leaving the property unassigned. + */ +NS_SWIFT_NAME(UserProfileChangeRequest) +@interface FIRUserProfileChangeRequest : NSObject + +/** @fn init + @brief Please use `FIRUser.profileChangeRequest` + */ +- (instancetype)init NS_UNAVAILABLE; + +// Only used for testing. +- (instancetype) + initWithCallbackManager:(firebase::testing::cppsdk::CallbackTickerManager *)callbackManager + NS_DESIGNATED_INITIALIZER; + +/** @property displayName + @brief The user's display name. + @remarks It is an error to set this property after calling + `FIRUserProfileChangeRequest.commitChangesWithCallback:` + */ +@property(nonatomic, copy, nullable) NSString *displayName; + +/** @property photoURL + @brief The user's photo URL. + @remarks It is an error to set this property after calling + `FIRUserProfileChangeRequest.commitChangesWithCallback:` + */ +@property(nonatomic, copy, nullable) NSURL *photoURL; + +/** @fn commitChangesWithCompletion: + @brief Commits any pending changes. + @remarks This method should only be called once. Once called, property values should not be + changed. + + @param completion Optionally; the block invoked when the user profile change has been applied. + Invoked asynchronously on the main thread in the future. + */ +- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUser.mm b/auth/src/ios/fake/FIRUser.mm new file mode 100644 index 0000000000..ab8967c879 --- /dev/null +++ b/auth/src/ios/fake/FIRUser.mm @@ -0,0 +1,161 @@ +/* + * Copyright 2017 Google + * + * 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 "testing/config_ios.h" +#include "testing/ticker_ios.h" +#include "testing/util_ios.h" + +#import "auth/src/ios/fake/FIRUser.h" + +#import "auth/src/ios/fake/FIRUserMetadata.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRUser { + // Manages callbacks for testing. + firebase::testing::cppsdk::CallbackTickerManager _callbackManager; +} + +// Properties from protocol need to be synthesized explicitly. +@synthesize providerID = _providerID; +@synthesize uid = _uid; +@synthesize displayName = _displayName; +@synthesize photoURL = _photoURL; +@synthesize email = _email; +@synthesize phoneNumber = _phoneNumber; + +- (instancetype)init { + self = [super init]; + if (self) { + _anonymous = YES; + _providerID = @"fake provider id"; + _uid = @"fake uid"; + _displayName = @"fake display name"; + _email = @"fake email"; + _phoneNumber = @"fake phone number"; + _metadata = [[FIRUserMetadata alloc] init]; + } + return self; +} + +- (void)updateEmail:(NSString *)email + completion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.updateEmail:completion:", + ^(NSError *_Nullable error) { + _email = email; + completion(error); + }); +} + +- (void)updatePassword:(NSString *)password + completion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.updatePassword:completion:", completion); +} + +- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneNumberCredential + completion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.updatePhoneNumberCredential:completion:", completion); +} + +- (FIRUserProfileChangeRequest *)profileChangeRequest { + return [[FIRUserProfileChangeRequest alloc] initWithCallbackManager:&_callbackManager]; +} + +- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.reloadWithCompletion:", completion); +} + +- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRUser.reauthenticateWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init]); +} + +- (void) + reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential + completion:(nullable FIRAuthDataResultCallback) completion { + _callbackManager.Add(@"FIRUser.reauthenticateAndRetrieveDataWithCredential:completion:", + completion, [[FIRAuthDataResult alloc] init]); +} + +- (void)getIDTokenResultWithCompletion:(nullable FIRAuthTokenResultCallback)completion {} + +- (void)getIDTokenResultForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenResultCallback)completion {} + +- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh + completion:(nullable FIRAuthTokenCallback)completion { + _callbackManager.Add(@"FIRUser.getIDTokenForcingRefresh:completion:", completion, + @"a fake token"); +} + +- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {} + +- (void)linkWithCredential:(FIRAuthCredential *)credential + completion:(nullable FIRAuthDataResultCallback)completion { + _callbackManager.Add(@"FIRUser.linkWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init]); +} + +- (void)sendEmailVerificationWithCompletion: + (nullable FIRSendEmailVerificationCallback)completion { + _callbackManager.Add(@"FIRUser.sendEmailVerificationWithCompletion:", completion); +} + +- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *) credential + completion:(nullable FIRAuthDataResultCallback) completion { + _callbackManager.Add(@"FIRUser.linkAndRetrieveDataWithCredential:completion:", completion, + [[FIRAuthDataResult alloc] init]); +} + +- (void)unlinkFromProvider:(NSString *)provider + completion:(nullable FIRAuthResultCallback)completion { + _callbackManager.Add(@"FIRUser.unlinkFromProvider:completion:", completion, + [[FIRUser alloc] init]); +} + +- (void)sendEmailVerificationWithActionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings + completion:(nullable FIRSendEmailVerificationCallback) + completion { +} + +- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager.Add(@"FIRUser.deleteWithCompletion:", completion); +} + +@end + +@implementation FIRUserProfileChangeRequest { + // Manages callbacks for testing. Does not own it. + firebase::testing::cppsdk::CallbackTickerManager *_callbackManager; +} + +- (instancetype) + initWithCallbackManager:(firebase::testing::cppsdk::CallbackTickerManager *)callbackManager { + self = [super init]; + if (self) { + _callbackManager = callbackManager; + } + return self; +} + +- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion { + _callbackManager->Add(@"FIRUserProfileChangeRequest.commitChangesWithCompletion:", completion); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUserInfo.h b/auth/src/ios/fake/FIRUserInfo.h new file mode 100644 index 0000000000..04eca495de --- /dev/null +++ b/auth/src/ios/fake/FIRUserInfo.h @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + @brief Represents user data returned from an identity provider. + */ +NS_SWIFT_NAME(UserInfo) +@protocol FIRUserInfo + +/** @property providerID + @brief The provider identifier. + */ +@property(nonatomic, copy, readonly) NSString *providerID; + +/** @property uid + @brief The provider's user ID for the user. + */ +@property(nonatomic, copy, readonly) NSString *uid; + +/** @property displayName + @brief The name of the user. + */ +@property(nonatomic, copy, readonly, nullable) NSString *displayName; + +/** @property photoURL + @brief The URL of the user's profile photo. + */ +@property(nonatomic, copy, readonly, nullable) NSURL *photoURL; + +/** @property email + @brief The user's email address. + */ +@property(nonatomic, copy, readonly, nullable) NSString *email; + +/** @property phoneNumber + @brief A phone number associated with the user. + @remarks This property is only available for users authenticated via phone number auth. + */ +@property(nonatomic, readonly, nullable) NSString *phoneNumber; + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUserMetadata.h b/auth/src/ios/fake/FIRUserMetadata.h new file mode 100644 index 0000000000..96b6eb1058 --- /dev/null +++ b/auth/src/ios/fake/FIRUserMetadata.h @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRUserMetadata + @brief A data class representing the metadata corresponding to a Firebase user. + */ +NS_SWIFT_NAME(UserMetadata) +@interface FIRUserMetadata : NSObject + +/** @property lastSignInDate + @brief Stores the last sign in date for the corresponding Firebase user. + */ +@property(copy, nonatomic, readonly, nullable) NSDate *lastSignInDate; + +/** @property creationDate + @brief Stores the creation date for the corresponding Firebase user. + */ +@property(copy, nonatomic, readonly, nullable) NSDate *creationDate; + +#if defined(FIREBASE_AUTH_TESTING) +- (instancetype)init; +#else +/** @fn init + @brief This class should not be initialized manually, an instance of this class can be obtained + from a Firebase user object. + */ +- (instancetype)init NS_UNAVAILABLE; +#endif // defined(FIREBASE_AUTH_TESTING) + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FIRUserMetadata.mm b/auth/src/ios/fake/FIRUserMetadata.mm new file mode 100644 index 0000000000..fd5aa88934 --- /dev/null +++ b/auth/src/ios/fake/FIRUserMetadata.mm @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import "auth/src/ios/fake/FIRUserMetadata.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRUserMetadata + +- (instancetype)init { + self = [super init]; + if (self) { + _lastSignInDate = [NSDate dateWithTimeIntervalSince1970:1]; + _creationDate = [NSDate dateWithTimeIntervalSince1970:1]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/auth/src/ios/fake/FirebaseAuth.h b/auth/src/ios/fake/FirebaseAuth.h new file mode 100644 index 0000000000..462d2ecf86 --- /dev/null +++ b/auth/src/ios/fake/FirebaseAuth.h @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +#import "FIRActionCodeSettings.h" +#import "FIRAdditionalUserInfo.h" +#import "FIRAuth.h" +#import "FIRAuthCredential.h" +#import "FIRAuthDataResult.h" +#import "FIRAuthErrors.h" +#import "FIRAuthTokenResult.h" +#import "FirebaseAuthVersion.h" +#import "FIREmailAuthProvider.h" +#import "FIRFacebookAuthProvider.h" +#import "FIRFederatedAuthProvider.h" +#import "FIRGameCenterAuthProvider.h" +#import "FIRGitHubAuthProvider.h" +#import "FIRGoogleAuthProvider.h" +#import "FIROAuthCredential.h" +#import "FIROAuthProvider.h" +#import "FIRTwitterAuthProvider.h" +#import "FIRUser.h" +#import "FIRUserInfo.h" +#import "FIRUserMetadata.h" + +#if TARGET_OS_IOS +#import "FIRAuthUIDelegate.h" +#import "FIRPhoneAuthCredential.h" +#import "FIRPhoneAuthProvider.h" +#import "FIRAuthAPNSTokenType.h" +#import "FIRAuthSettings.h" +#endif diff --git a/auth/src/ios/fake/FirebaseAuthVersion.h b/auth/src/ios/fake/FirebaseAuthVersion.h new file mode 100644 index 0000000000..7b4b94e908 --- /dev/null +++ b/auth/src/ios/fake/FirebaseAuthVersion.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#import + +/** + Version number for FirebaseAuth. + */ +extern const double FirebaseAuthVersionNum; + +/** + Version string for FirebaseAuth. + */ +extern const char *const FirebaseAuthVersionStr; diff --git a/auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java b/auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java new file mode 100644 index 0000000000..263e3035e2 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/FirebaseApiNotAvailableException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseApiNotAvailableException */ +public class FirebaseApiNotAvailableException extends FirebaseException { + + public FirebaseApiNotAvailableException(String message) { + super(message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java b/auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java new file mode 100644 index 0000000000..80571d4871 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/FirebaseNetworkException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseNetworkException */ +public class FirebaseNetworkException extends FirebaseException { + + public FirebaseNetworkException(String message) { + super(message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java b/auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java new file mode 100644 index 0000000000..339c566a92 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/FirebaseTooManyRequestsException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase; + +/** Fake FirebaseTooManyRequestsException */ +public class FirebaseTooManyRequestsException extends FirebaseException { + + public FirebaseTooManyRequestsException(String message) { + super(message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java b/auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java new file mode 100644 index 0000000000..c4eaeb29cf --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/AdditionalUserInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import java.util.Map; + +/** Fake AdditionalUserInfo */ +public final class AdditionalUserInfo { + + public String getProviderId() { + return "fake provider id"; + } + + public Map getProfile() { + return null; + } + + public String getUsername() { + return "fake user name"; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/AuthCredential.java b/auth/src_java/fake/com/google/firebase/auth/AuthCredential.java new file mode 100644 index 0000000000..8022311410 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/AuthCredential.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake AuthCredential */ +public final class AuthCredential { + private String provider; + + /** C++ code does not rely on any constructor. This is solely for fake to specify provider and + * does not map to a constructor in the real AuthCredential. */ + AuthCredential(String provider) { + this.provider = provider; + } + + public String getSignInMethod() { + return this.provider; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/AuthResult.java b/auth/src_java/fake/com/google/firebase/auth/AuthResult.java new file mode 100644 index 0000000000..fa3bf9fa92 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/AuthResult.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake AuthResult */ +public final class AuthResult { + + FirebaseUser getUser() { + return new FirebaseUser(); + } + + AdditionalUserInfo getAdditionalUserInfo() { + return new AdditionalUserInfo(); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java new file mode 100644 index 0000000000..c785cfb7aa --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/EmailAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake EmailAuthProvider */ +public final class EmailAuthProvider { + + public static AuthCredential getCredential(String email, String password) { + return new AuthCredential("password"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java new file mode 100644 index 0000000000..ee9997561b --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FacebookAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FacebookAuthProvider */ +public final class FacebookAuthProvider { + + public static AuthCredential getCredential(String accessToken) { + return new AuthCredential("facebook.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java new file mode 100644 index 0000000000..b53120cfdc --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FederatedAuthProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ +package com.google.firebase.auth; + +/** + * Abstract representation of an arbitrary federated authentication provider. Generate instances + * using {@link OAuthProvider.Builder}. + */ +public abstract class FederatedAuthProvider {} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java new file mode 100644 index 0000000000..63b3b1ffa6 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuth.java @@ -0,0 +1,242 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.app.Activity; +import com.google.android.gms.tasks.Task; +import com.google.firebase.FirebaseApiNotAvailableException; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseNetworkException; +import com.google.firebase.FirebaseTooManyRequestsException; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeListener; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import java.util.ArrayList; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Fake FirebaseAuth */ +public final class FirebaseAuth { + // This makes the signed-in status consistent and thus makes setting up test data config easier. + private boolean signedIn = false; + + // Random number generator for listener callback delays. + private final Random randomDelay = new Random(); + + private final ArrayList authStateListeners = new ArrayList<>(); + private final ArrayList idTokenListeners = new ArrayList<>(); + + public static FirebaseAuth getInstance(FirebaseApp firebaseApp) { + return new FirebaseAuth(); + } + + public FirebaseUser getCurrentUser() { + if (signedIn) { + return new FirebaseUser(); + } else { + return null; + } + } + + public static Task applyAuthExceptionFromConfig(Task task, String exceptionMsg) { + Pattern r = Pattern.compile("^\\[(.*)[?!:]:?(.*)\\] (.*)"); + Matcher matcher = r.matcher(exceptionMsg); + if (matcher.find()) { + String exceptionName = matcher.group(1); + String errorCode = matcher.group(2); + String errorMessage = matcher.group(3); + if (exceptionName.equals("FirebaseAuthInvalidCredentialsException")) { + task.setException(new FirebaseAuthInvalidCredentialsException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthActionCodeException")) { + task.setException(new FirebaseAuthActionCodeException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthEmailException")) { + task.setException(new FirebaseAuthEmailException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthException")) { + task.setException(new FirebaseAuthException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthInvalidUserException")) { + task.setException(new FirebaseAuthInvalidUserException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthRecentLoginRequiredException")) { + task.setException(new FirebaseAuthRecentLoginRequiredException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthUserCollisionException")) { + task.setException(new FirebaseAuthUserCollisionException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseAuthWeakPasswordException")) { + task.setException(new FirebaseAuthWeakPasswordException(errorCode, errorMessage)); + } else if (exceptionName.equals("FirebaseApiNotAvailableException")) { + task.setException(new FirebaseApiNotAvailableException(errorMessage)); + } else if (exceptionName.equals("FirebaseException")) { + task.setException(new FirebaseException(errorMessage)); + } else if (exceptionName.equals("FirebaseNetworkException")) { + task.setException(new FirebaseNetworkException(errorMessage)); + } else if (exceptionName.equals("FirebaseTooManyRequestsException")) { + task.setException(new FirebaseTooManyRequestsException(errorMessage)); + } + } + return task; + } + + /** + * Delay the calling thread between 0..100ms. + */ + private void randomDelayThread() { + try { + Thread.sleep(randomDelay.nextInt(100)); + } catch (InterruptedException e) { + // ignore + } + } + + public void addAuthStateListener(final AuthStateListener listener) { + authStateListeners.add(listener); + new Thread( + new Runnable() { + @Override + public void run() { + randomDelayThread(); + listener.onAuthStateChanged(FirebaseAuth.this); + } + }) + .start(); + } + + public void removeAuthStateListener(AuthStateListener listener) { + authStateListeners.remove(listener); + } + + public void addIdTokenListener(final IdTokenListener listener) { + idTokenListeners.add(listener); + new Thread( + new Runnable() { + @Override + public void run() { + randomDelayThread(); + listener.onIdTokenChanged(FirebaseAuth.this); + } + }) + .start(); + } + + public void removeIdTokenListener(IdTokenListener listener) { + idTokenListeners.remove(listener); + } + + public void signOut() { + signedIn = false; + } + + public Task fetchSignInMethodsForEmail(String email) { + return null; + } + + /** A generic helper function to mimic all types of sign-in actions. */ + private Task signInHelper(String configKey) { + Task result = Task.forResult(configKey, new AuthResult()); + + ConfigRow row = ConfigAndroid.get(configKey); + if (!row.futuregeneric().throwexception()) { + result.addListener(new FakeListener() { + @Override + public void onSuccess(AuthResult res) { + signedIn = true; + for (AuthStateListener listener : authStateListeners) { + listener.onAuthStateChanged(FirebaseAuth.this); + } + } + }); + } else { + result = applyAuthExceptionFromConfig(result, row.futuregeneric().exceptionmsg()); + } + + TickerAndroid.register(result); + return result; + } + + public Task signInWithCustomToken(String token) { + return signInHelper("FirebaseAuth.signInWithCustomToken"); + } + + public Task signInWithCredential(AuthCredential credential) { + return signInHelper("FirebaseAuth.signInWithCredential"); + } + + public Task signInAnonymously() { + return signInHelper("FirebaseAuth.signInAnonymously"); + } + + public Task signInWithEmailAndPassword(String email, String password) { + return signInHelper("FirebaseAuth.signInWithEmailAndPassword"); + } + + /** + * Signs in the user using the mobile browser (either a Custom Chrome Tab or the device's default + * browser) for the given {@code provider}. + * + *

    Note: this call has a UI associated with it, unlike the majority of calls in FirebaseAuth. + * + *

    Exceptions
    + * + *
      + *
    • {@link FirebaseAuthInvalidCredentialsException} thrown if the credential generated from + * the flow is malformed or expired. + *
    • {@link FirebaseAuthInvalidUserException} thrown if the user has been disabled by an + * administrator. + *
    • {@link FirebaseAuthUserCollisionException} thrown if the email that keys the user that is + * signing in is already in use. + *
    • {@link FirebaseAuthWebException} thrown if there is an operation already in progress, the + * pending operation was canceled, there is a problem with 3rd party cookies in the browser, + * or some other error in the web context has occurred. + *
    • {@link FirebaseAuthException} thrown if signing in via this method has been disabled in + * the Firebase Console, or if the {@code provider} passed is configured improperly. + *
    + * + * @param activity the current {@link Activity} from which you intend to launch this flow. + * @param federatedAuthProvider an {@link FederatedAuthProvider} configured with information about + * how you intend the user to sign in. + * @return a {@link Task} with a reference to an {@link AuthResult} with user information upon + * success + */ + public Task startActivityForSignInWithProvider( + Activity activity, FederatedAuthProvider federatedAuthProvider) { + return signInHelper("FirebaseAuth.startActivityForSignInWithProvider"); + } + + public Task createUserWithEmailAndPassword(String email, String password) { + return signInHelper("FirebaseAuth.createUserWithEmailAndPassword"); + } + + public Task sendPasswordResetEmail(String email) { + Task result = Task.forResult("FirebaseAuth.sendPasswordResetEmail", null); + ConfigRow row = ConfigAndroid.get("FirebaseAuth.sendPasswordResetEmail"); + if (row.futuregeneric().throwexception()) { + result = applyAuthExceptionFromConfig(result, row.futuregeneric().exceptionmsg()); + } + TickerAndroid.register(result); + return result; + } + + /** AuthStateListener */ + public interface AuthStateListener { + void onAuthStateChanged(FirebaseAuth auth); + } + + /** IdTokenListener */ + public interface IdTokenListener { + void onIdTokenChanged(FirebaseAuth auth); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java new file mode 100644 index 0000000000..30cc2e5398 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthActionCodeException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthActionCodeException */ +public class FirebaseAuthActionCodeException extends FirebaseAuthException { + + public FirebaseAuthActionCodeException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java new file mode 100644 index 0000000000..e1d46ad849 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthEmailException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthEmailException */ +public class FirebaseAuthEmailException extends FirebaseAuthException { + + public FirebaseAuthEmailException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java new file mode 100644 index 0000000000..f519095c5a --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import com.google.firebase.FirebaseException; + +/** Fake FirebaseAuthException */ +public class FirebaseAuthException extends FirebaseException { + + public FirebaseAuthException(String code, String message) { + super(message); + code_ = code; + } + + public String getErrorCode() { + return code_; + } + + private String code_; +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java new file mode 100644 index 0000000000..8e37cda351 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidCredentialsException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthInvalidCredentialsException */ +public class FirebaseAuthInvalidCredentialsException extends FirebaseAuthException { + + public FirebaseAuthInvalidCredentialsException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java new file mode 100644 index 0000000000..30566fa193 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthInvalidUserException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthInvalidUserException */ +public final class FirebaseAuthInvalidUserException extends FirebaseAuthException { + + public FirebaseAuthInvalidUserException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java new file mode 100644 index 0000000000..e1a7cecd13 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthRecentLoginRequiredException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthRecentLoginRequiredException */ +public class FirebaseAuthRecentLoginRequiredException extends FirebaseAuthException { + + public FirebaseAuthRecentLoginRequiredException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java new file mode 100644 index 0000000000..63e94ce39d --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthUserCollisionException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthUserCollisionException */ +public class FirebaseAuthUserCollisionException extends FirebaseAuthException { + + public FirebaseAuthUserCollisionException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java new file mode 100644 index 0000000000..acfb84ebc5 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWeakPasswordException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthWeakPasswordException */ +public class FirebaseAuthWeakPasswordException extends FirebaseAuthInvalidCredentialsException { + + public FirebaseAuthWeakPasswordException(String code, String message) { + super(code, message); + } + + public String getReason() { + return "fake bad password reason."; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java new file mode 100644 index 0000000000..8260a51a76 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseAuthWebException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake FirebaseAuthWebException */ +public class FirebaseAuthWebException extends FirebaseAuthException { + + public FirebaseAuthWebException(String code, String message) { + super(code, message); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java new file mode 100644 index 0000000000..753371f789 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseUser.java @@ -0,0 +1,201 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.app.Activity; +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeListener; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import java.util.List; + +/** Fake FirebaseUser (not completed yet). */ +public final class FirebaseUser extends UserInfo { + public boolean isAnonymous() { + return true; + } + + public Task getIdToken(boolean forceRefresh) { + Task result = Task.forResult("FirebaseUser.getIdToken", new GetTokenResult()); + TickerAndroid.register(result); + return result; + } + + public List getProviderData() { + return null; + } + + public Task updateEmail(String email) { + final String configKey = "FirebaseUser.updateEmail"; + Task result = Task.forResult(configKey, null); + + ConfigRow row = ConfigAndroid.get(configKey); + if (!row.futuregeneric().throwexception()) { + result.addListener( + new FakeListener() { + @Override + public void onSuccess(Void res) { + FirebaseUser.this.email = email; + } + }); + } + + TickerAndroid.register(result); + return result; + } + + public Task updatePassword(String email) { + Task result = Task.forResult("FirebaseUser.updatePassword", null); + TickerAndroid.register(result); + return result; + } + + public Task updateProfile(UserProfileChangeRequest request) { + Task result = Task.forResult("FirebaseUser.updateProfile", null); + TickerAndroid.register(result); + return result; + } + + public Task linkWithCredential(AuthCredential credential) { + Task result = Task.forResult("FirebaseUser.linkWithCredential", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + /** + * Links the user using the mobile browser (either a Custom Chrome Tab or the device's default + * browser) to the given {@code provider}. If the calling activity dies during this operation, use + * {@link FirebaseAuth#getPendingAuthResult()} to get the outcome of this operation. + * + *

    Note: this call has a UI associated with it, unlike the majority of calls in FirebaseAuth. + * + *

    Exceptions
    + * + *
      + *
    • {@link FirebaseAuthInvalidCredentialsException} thrown if the credential generated from + * the flow is malformed or expired. + *
    • {@link FirebaseAuthInvalidUserException} thrown if the user has been disabled by an + * administrator. + *
    • {@link FirebaseAuthUserCollisionException} thrown if the email that keys the user that is + * signing in is already in use. + *
    • {@link FirebaseAuthWebException} thrown if there is an operation already in progress, the + * pending operation was canceled, there is a problem with 3rd party cookies in the browser, + * or some other error in the web context has occurred. + *
    • {@link FirebaseAuthException} thrown if signing in via this method has been disabled in + * the Firebase Console, or if the {@code provider} passed is configured improperly. + *
    + * + * @param activity the current {@link Activity} that you intent to launch this flow from + * @param federatedAuthProvider an {@link FederatedAuthProvider} configured with information about + * the provider that you intend to link to the user. + * @return a {@link Task} with a reference to an {@link AuthResult} with user information upon + * success + */ + public Task startActivityForLinkWithProvider( + Activity activity, FederatedAuthProvider federatedAuthProvider) { + Task result = + Task.forResult("FirebaseUser.startActivityForLinkWithProvider", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + public Task unlink(String provider) { + Task result = Task.forResult("FirebaseUser.unlink", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + public Task updatePhoneNumber(PhoneAuthCredential credential) { + return null; + } + + public Task reload() { + Task result = Task.forResult("FirebaseUser.reload", null); + TickerAndroid.register(result); + return result; + } + + public Task reauthenticate(AuthCredential credential) { + Task result = Task.forResult("FirebaseUser.reauthenticate", null); + TickerAndroid.register(result); + return result; + } + + public Task reauthenticateAndRetrieveData(AuthCredential credential) { + Task result = + Task.forResult("FirebaseUser.reauthenticateAndRetrieveData", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + /** + * Reauthenticates the user using the mobile browser (either a Custom Chrome Tab or the device's + * default browser) using the given {@code provider}. If the calling activity dies during this + * operation, use {@link FirebaseAuth#getPendingAuthResult()} to get the outcome of this + * operation. + * + *

    Note: this call has a UI associated with it, unlike the majority of calls in FirebaseAuth. + * + *

    Exceptions
    + * + *
      + *
    • {@link FirebaseAuthInvalidCredentialsException} thrown if the credential generated from + * the flow is malformed or expired. + *
    • {@link FirebaseAuthInvalidUserException} thrown if the user has been disabled by an + * administrator. + *
    • {@link FirebaseAuthUserCollisionException} thrown if the email that keys the user that is + * signing in is already in use. + *
    • {@link FirebaseAuthWebException} thrown if there is an operation already in progress, the + * pending operation was canceled, there is a problem with 3rd party cookies in the browser, + * or some other error in the web context has occurred. + *
    • {@link FirebaseAuthException} thrown if signing in via this method has been disabled in + * the Firebase Console, or if the {@code provider} passed is configured improperly. + *
    + * + * @param activity the current {@link Activity} that you intent to launch this flow from + * @param federatedAuthProvider an {@link FederatedAuthProvider} configured with information about + * how you intend the user to reauthenticate. + * @return a {@link Task} with a reference to an {@link AuthResult} with user information upon + * success + */ + public Task startActivityForReauthenticateWithProvider( + Activity activity, FederatedAuthProvider federatedAuthProvider) { + Task result = + Task.forResult("FirebaseUser.startActivityForReauthenticateWithProvider", new AuthResult()); + TickerAndroid.register(result); + return result; + } + + + public Task delete() { + Task result = Task.forResult("FirebaseUser.delete", null); + TickerAndroid.register(result); + return result; + } + + public Task sendEmailVerification() { + Task result = Task.forResult("FirebaseUser.sendEmailVerification", null); + TickerAndroid.register(result); + return result; + } + + /** Returns the {@link FirebaseUserMetadata} associated with this user. */ + public FirebaseUserMetadata getMetadata() { + return new FirebaseUserMetadata(); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java b/auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java new file mode 100644 index 0000000000..6f214cad5b --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/FirebaseUserMetadata.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Holds the user metadata for the current {@link FirebaseUser} */ +public class FirebaseUserMetadata { + + /** Fake timestamp returned that's non-zero. */ + public long getLastSignInTimestamp() { + return 1; + } + + /** Fake timestamp returned that's non-zero. */ + public long getCreationTimestamp() { + return 1; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java b/auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java new file mode 100644 index 0000000000..b925fc7627 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/GetTokenResult.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake GetTokenResult */ +public final class GetTokenResult { + + public String getToken() { + return "a fake token"; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java new file mode 100644 index 0000000000..8f08b9df4c --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/GithubAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake GithubAuthProvider */ +public final class GithubAuthProvider { + + public static AuthCredential getCredential(String accessToken) { + return new AuthCredential("github.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java new file mode 100644 index 0000000000..ad9b327934 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/GoogleAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake GoogleAuthProvider */ +public final class GoogleAuthProvider { + + public static AuthCredential getCredential(String idToken, String accessToken) { + return new AuthCredential("google.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java new file mode 100644 index 0000000000..32867a4aca --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/OAuthProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import java.util.List; +import java.util.Map; + +/** Fake FakeOAuthProvider */ +public final class OAuthProvider extends FederatedAuthProvider { + + public static AuthCredential getCredential( + String providerId, String idToken, String accessToken) { + return new AuthCredential(providerId); + } + + /** + * Returns a {@link OAuthProvider.Builder} used to construct a {@link OAuthProvider} instantiated + * with the given {@code providerId}. + */ + public static OAuthProvider.Builder newBuilder(String providerId, FirebaseAuth firebaseAuth) { + return new OAuthProvider.Builder(); + } + + /** Class used to create instances of {@link OAuthProvider}. */ + public static class Builder { + + /* Fake constructor */ + private Builder() {} + + /** + * Sets the OAuth 2 scopes to be presented to the user during their sign-in flow with the + * identity provider. + */ + public OAuthProvider.Builder setScopes(List scopes) { + return this; + } + + /** + * Configures custom parameters to be passed to the identity provider during the OAuth sign-in + * flow. Calling this method multiple times will add to the set of custom parameters being + * passed, rather than overwriting them (as long as key values don't collide). + * + * @param paramKey the name of the custom parameter + * @param paramValue the value of the custom parameter + */ + public OAuthProvider.Builder addCustomParameter(String paramKey, String paramValue) { + return this; + } + + /** + * Similar to {@link #addCustomParameter(String, String)}, this takes a Map and adds each entry + * to the set of custom parameters to be passed. Calling this method multiple times will add to + * the set of custom parameters being passed, rather than overwriting them (as long as key + * values don't collide). + * + * @param customParameters a dictionary of custom parameter names and values to be passed to the + * identity provider as part of the sign-in flow. + */ + public OAuthProvider.Builder addCustomParameters(Map customParameters) { + return this; + } + + /** Returns an {@link OAuthProvider} created from this {@link Builder}. */ + public OAuthProvider build() { + return new OAuthProvider(); + } + } + + /** + * Creates an {@link OAuthProvider.CredentialBuilder} for the specified provider ID. + * + * @throws IllegalArgumentException if {@code providerId} is null or empty + */ + public static CredentialBuilder newCredentialBuilder(String providerId) { + return new CredentialBuilder(providerId); + } + + /** Builder class to initialize {@link AuthCredential}'s. */ + public static class CredentialBuilder { + + private final String providerId; + + /** + * Internal constructor. + */ + private CredentialBuilder(String providerId) { + this.providerId = providerId; + } + + /** + * Adds an ID token to the credential being built. + * + *

    If this is an OIDC ID token with a nonce field, please use {@link + * #setIdTokenWithRawNonce(String, String)} instead. + */ + public OAuthProvider.CredentialBuilder setIdToken(String idToken) { + return this; + } + + /** + * Adds an ID token and raw nonce to the credential being built. + * + *

    The raw nonce is required when an OIDC ID token with a nonce field is provided. The + * SHA-256 hash of the raw nonce must match the nonce field in the OIDC ID token. + */ + public OAuthProvider.CredentialBuilder setIdTokenWithRawNonce(String idToken, String rawNonce) { + return this; + } + + /** Adds an access token to the credential being built. */ + public OAuthProvider.CredentialBuilder setAccessToken(String accessToken) { + return this; + } + + /** + * Returns the {@link AuthCredential} that this {@link CredentialBuilder} has constructed. + * + * @throws IllegalArgumentException if an ID token and access token were not provided. + */ + public AuthCredential build() { + return new AuthCredential(providerId); + } + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java new file mode 100644 index 0000000000..a55d82a58e --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthCredential.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake PhoneAuthCredential */ +public class PhoneAuthCredential { + public String getSmsCode() { + return "fake sms code"; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java new file mode 100644 index 0000000000..84b10a73a9 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/PhoneAuthProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.app.Activity; +import com.google.firebase.FirebaseException; +import java.util.concurrent.TimeUnit; + +/** Fake PhoneAuthProvider */ +public class PhoneAuthProvider { + + /** Fake OnVerificationStateChangedCallbacks */ + public abstract static class OnVerificationStateChangedCallbacks { + public abstract void onVerificationCompleted(PhoneAuthCredential credential); + + public abstract void onVerificationFailed(FirebaseException exception); + + public void onCodeSent(String verificationId, ForceResendingToken forceResendingToken) {} + + public void onCodeAutoRetrievalTimeOut(String verificationId) {} + } + + /** Fake ForceResendingToken */ + public static class ForceResendingToken {} + + public static PhoneAuthProvider getInstance(FirebaseAuth firebaseAuth) { + return null; + } + + public static PhoneAuthCredential getCredential( + String verificationId, String smsCode) { + return null; + } + + public void verifyPhoneNumber( + String phoneNumber, + long timeout, + TimeUnit unit, + Activity activity, + OnVerificationStateChangedCallbacks callbacks, + ForceResendingToken forceResendingToken) {} +} diff --git a/auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java new file mode 100644 index 0000000000..b5da4c3cf2 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/PlayGamesAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake PlayGamesAuthProvider */ +class PlayGamesAuthProvider { + + public static AuthCredential getCredential(String authCode) { + return new AuthCredential("playgames.google.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java b/auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java new file mode 100644 index 0000000000..d0ba463a8f --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/SignInMethodQueryResult.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import java.util.List; + +/** Fake SignInMethodQueryResult */ +public final class SignInMethodQueryResult { + + List getSignInMethods() { + return null; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java b/auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java new file mode 100644 index 0000000000..c3358e7c20 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/TwitterAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +/** Fake TwitterAuthProvider */ +public final class TwitterAuthProvider { + + public static AuthCredential getCredential(String token, String secret) { + return new AuthCredential("twitter.com"); + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/UserInfo.java b/auth/src_java/fake/com/google/firebase/auth/UserInfo.java new file mode 100644 index 0000000000..f2570abb2d --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/UserInfo.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.net.Uri; + +/** Fake UserInfo */ +public class UserInfo { + protected String email = "fake email"; + + String getUid() { + return "fake uid"; + } + + String getProviderId() { + return "fake provider id"; + } + + String getDisplayName() { + return "fake display name"; + } + + String getPhoneNumber() { + return "fake phone number"; + } + + Uri getPhotoUrl() { + return null; + } + + String getEmail() { + return email; + } + + boolean isEmailVerified() { + // This is false to match the desktop stub. + return false; + } +} diff --git a/auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java b/auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java new file mode 100644 index 0000000000..dfab7c05b0 --- /dev/null +++ b/auth/src_java/fake/com/google/firebase/auth/UserProfileChangeRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +package com.google.firebase.auth; + +import android.net.Uri; + +/** Fake UserProfileChangeRequest$Builder */ +public final class UserProfileChangeRequest { + + /** Builder */ + public static class Builder { + public Builder setDisplayName(String displayName) { + return this; + } + + public Builder setPhotoUri(Uri photoUri) { + return this; + } + + public UserProfileChangeRequest build() { + return new UserProfileChangeRequest(); + } + } +} diff --git a/auth/tests/CMakeLists.txt b/auth/tests/CMakeLists.txt new file mode 100644 index 0000000000..15f5fd6820 --- /dev/null +++ b/auth/tests/CMakeLists.txt @@ -0,0 +1,282 @@ +# Copyright 2019 Google LLC +# +# 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. + +set(desktop_fakes_SRCS + desktop/fakes.h + desktop/fakes.cc) +set(desktop_test_util_SRCS + desktop/test_utils.h + desktop/test_utils.cc + ) +set(ios_frameworks + FirebaseAuth + ) + +add_library(firebase_auth_desktop_test_util STATIC + ${desktop_fakes_SRCS} + ${desktop_test_util_SRCS}) + +target_include_directories(firebase_auth_desktop_test_util + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_GEN_FILE_DIR} +) + +target_link_libraries(firebase_auth_desktop_test_util + PRIVATE + firebase_auth + firebase_testing + gtest + gmock +) + +target_compile_definitions(firebase_auth_desktop_test_util + PRIVATE + -DINTERNAL_EXPERIMENTAL=1 +) + + +if (NOT ANDROID AND NOT IOS) + set(desktop_rpc_test_util_SRCS + desktop/rpcs/test_util.h + desktop/rpcs/test_util.cc) + + add_library(firebase_auth_desktop_rpc_test_util STATIC + ${desktop_rpc_test_util_SRCS}) + + target_include_directories(firebase_auth_desktop_rpc_test_util + PRIVATE + ${FLATBUFFERS_SOURCE_DIR}/include + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_GEN_FILE_DIR} + ) + + target_link_libraries(firebase_auth_desktop_rpc_test_util + PRIVATE + firebase_auth + ) +endif() + + +firebase_cpp_cc_test( + firebase_auth_test + SOURCES + auth_test.cc + DEPENDS + firebase_app_for_testing + firebase_rest_mocks + firebase_auth + firebase_testing + DEFINES + -DFIREBASE_WAIT_ASYNC_IN_TEST +) + +firebase_cpp_cc_test_on_ios( + firebase_auth_test + HOST + firebase_app_for_testing_ios + SOURCES + credential_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing + CUSTOM_FRAMEWORKS + ${ios_frameworks} +) + +firebase_cpp_cc_test( + firebase_auth_credential_test + SOURCES + credential_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test_on_ios( + firebase_auth_credential_test + HOST + firebase_app_for_testing_ios + SOURCES + credential_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing + CUSTOM_FRAMEWORKS + ${ios_frameworks} +) + +firebase_cpp_cc_test( + firebase_auth_user_test + SOURCES + user_test.cc + DEPENDS + firebase_app_for_testing + firebase_rest_mocks + firebase_auth + firebase_testing + DEFINES + -DFIREBASE_WAIT_ASYNC_IN_TEST +) + +firebase_cpp_cc_test_on_ios( + firebase_auth_user_test + HOST + firebase_app_for_testing_ios + SOURCES + user_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing + CUSTOM_FRAMEWORKS + ${ios_frameworks} +) + +firebase_cpp_cc_test( + firebase_auth_desktop_test + SOURCES + desktop/auth_desktop_test.cc + DEPENDS + firebase_auth + firebase_auth_desktop_test_util + firebase_rest_lib + firebase_rest_mocks + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_user_desktop_test + SOURCES + desktop/user_desktop_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_auth_desktop_test_util + firebase_rest_lib + firebase_rest_mocks + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_create_auth_uri_test + SOURCES + desktop/rpcs/create_auth_uri_test.cc + DEPENDS + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_delete_account_test_test + SOURCES + desktop/rpcs/delete_account_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_get_account_info_test + SOURCES + desktop/rpcs/get_account_info_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_get_oob_confirmation_code_test + SOURCES + desktop/rpcs/get_oob_confirmation_code_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_reset_password_test + SOURCES + desktop/rpcs/reset_password_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_secure_token_test + SOURCES + desktop/rpcs/secure_token_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_set_account_info_test + SOURCES + desktop/rpcs/set_account_info_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_sign_up_new_user_test + SOURCES + desktop/rpcs/sign_up_new_user_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_verify_assertion_test + SOURCES + desktop/rpcs/verify_assertion_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_verify_custom_token_test + SOURCES + desktop/rpcs/verify_custom_token_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) + +firebase_cpp_cc_test( + firebase_auth_verify_password_test + SOURCES + desktop/rpcs/verify_password_test.cc + DEPENDS + firebase_app_for_testing + firebase_auth + firebase_testing +) diff --git a/auth/tests/auth_test.cc b/auth/tests/auth_test.cc new file mode 100644 index 0000000000..1f61752446 --- /dev/null +++ b/auth/tests/auth_test.cc @@ -0,0 +1,558 @@ +/* + * Copyright 2017 Google LLC + * + * 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 + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/include/firebase/auth.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/ticker.h" + +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) +#include "app/rest/transport_builder.h" +#include "app/rest/transport_mock.h" +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +namespace firebase { +namespace auth { + +namespace { + +// Wait for the Future completed when necessary. We do not do so for Android nor +// iOS since their test is based on Ticker-based fake. We do not do so for +// desktop stub since its Future completes immediately. +template +inline void MaybeWaitForFuture(const Future& future) { +// Desktop developer sdk has a small delay due to async calls. +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + // Once REST implementation is in, we should be able to check this. Almost + // always the return of last-result is ahead of the future completion. But + // right now, the return of last-result actually happens after future is + // completed. + // EXPECT_EQ(firebase::kFutureStatusPending, future.status()); + while (firebase::kFutureStatusPending == future.status()) {} +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) +} + +// Helper functions to verify the auth future result. +template +void Verify(const AuthError error, const Future& result, + bool check_result_not_null) { +// Desktop stub returns result immediately and thus we skip the ticker elapse. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + MaybeWaitForFuture(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(error, result.error()); + if (check_result_not_null) { + EXPECT_NE(nullptr, result.result()); + } +} + +template +void Verify(const AuthError error, const Future& result) { + Verify(error, result, true /* check_result_not_null */); +} + +template <> +void Verify(const AuthError error, const Future& result) { + Verify(error, result, false /* check_result_not_null */); +} + +} // anonymous namespace + +class AuthTest : public ::testing::Test { + protected: + void SetUp() override { +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + } + + void TearDown() override { + delete firebase_auth_; + firebase_auth_ = nullptr; + delete firebase_app_; + firebase_app_ = nullptr; + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + // Helper function for those test case that needs an Auth but not care on the + // creation of that. + void MakeAuth() { + firebase_app_ = testing::CreateApp(); + firebase_auth_ = Auth::GetAuth(firebase_app_); + } + + App* firebase_app_ = nullptr; + Auth* firebase_auth_ = nullptr; +}; + +TEST_F(AuthTest, TestAuthCreation) { + // This test verifies the creation of an Auth object. + App* firebase_app = testing::CreateApp(); + EXPECT_NE(nullptr, firebase_app); + + Auth* firebase_auth = Auth::GetAuth(firebase_app); + EXPECT_NE(nullptr, firebase_auth); + + // Calling again does not create a new Auth object. + Auth* firebase_auth_again = Auth::GetAuth(firebase_app); + EXPECT_EQ(firebase_auth, firebase_auth_again); + + delete firebase_auth; + delete firebase_app; +} + +// Creates and destroys multiple auth objects to ensure destruction doesn't +// result in data races due to callbacks from the Java layer. +TEST_F(AuthTest, TestAuthCreateDestroy) { + static int kTestIterations = 100; + // Pipeline of app and auth objects that are all active at once. + struct { + App *app; + Auth *auth; + } created_queue[10]; + memset(created_queue, 0, sizeof(created_queue)); + size_t created_queue_items = sizeof(created_queue) / sizeof(created_queue[0]); + + // Create and destroy app and auth objects keeping up to created_queue_items + // alive at a time. + for (size_t i = 0; i < kTestIterations; ++i) { + auto* queue_entry = &created_queue[i % created_queue_items]; + delete queue_entry->auth; + delete queue_entry->app; + queue_entry->app = + testing::CreateApp(testing::MockAppOptions(), + (std::string("app") + std::to_string(i)).c_str()); + queue_entry->auth = Auth::GetAuth(queue_entry->app); + EXPECT_NE(nullptr, queue_entry->auth); + } + + // Clean up the queue. + for (size_t i = 0; i < created_queue_items; ++i) { + auto* queue_entry = &created_queue[i % created_queue_items]; + delete queue_entry->auth; + queue_entry->auth = nullptr; + delete queue_entry->app; + queue_entry->app = nullptr; + } +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +TEST_F(AuthTest, TestAuthCreationWithNoGooglePlay) { + // This test is specific to Android platform. Without Google Play, we cannot + // create an Auth object. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'GoogleApiAvailability.isGooglePlayServicesAvailable'," + " futureint:{value:1}}" + " ]" + "}"); + App* firebase_app = testing::CreateApp(); + EXPECT_NE(nullptr, firebase_app); + + Auth* firebase_auth = Auth::GetAuth(firebase_app); + EXPECT_EQ(nullptr, firebase_auth); + + delete firebase_app; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +// Below are tests for testing different login methods and in different status. + +TEST_F(AuthTest, TestSignInWithCustomTokenSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCustomToken'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInWithCustomToken:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyCustomToken?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInWithCustomToken("its-a-token"); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestSignInWithCredentialSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCredential'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInWithCredential:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Credential credential = EmailAuthProvider::GetCredential("abc@g.com", "abc"); + Future result = firebase_auth_->SignInWithCredential(credential); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestSignInAnonymouslySucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInAnonymously(); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestSignInWithEmailAndPasswordSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithEmailAndPassword'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.signInWithEmail:password:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->SignInWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorNone, result); +} + +TEST_F(AuthTest, TestCreateUserWithEmailAndPasswordSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.createUserWithEmailAndPassword'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.createUserWithEmail:password:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->CreateUserWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorNone, result); +} + +// Right now the desktop stub always succeeded. We could potentially test it by +// adding a desktop fake, which does not provide much value for the specific +// case of Auth since the C++ code is only a thin wraper. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + +TEST_F(AuthTest, TestSignInWithCustomTokenFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCustomToken'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "CUSTOM_TOKEN] sign-in with custom token failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInWithCustomToken:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "CUSTOM_TOKEN] sign-in with custom token failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInWithCustomToken("its-a-token"); + Verify(kAuthErrorInvalidCustomToken, result); +} + +TEST_F(AuthTest, TestSignInWithCredentialFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithCredential'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "EMAIL] sign-in with credential failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInWithCredential:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_INVALID_" + "EMAIL] sign-in with credential failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Credential credential = EmailAuthProvider::GetCredential("abc@g.com", "abc"); + Future result = firebase_auth_->SignInWithCredential(credential); + Verify(kAuthErrorInvalidEmail, result); +} + +TEST_F(AuthTest, TestSignInAnonymouslyFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthException:ERROR_OPERATION_NOT_ALLOWED] " + "sign-in anonymously failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthException:ERROR_OPERATION_NOT_ALLOWED] " + "sign-in anonymously failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SignInAnonymously(); + Verify(kAuthErrorOperationNotAllowed, result); +} + +TEST_F(AuthTest, TestSignInWithEmailAndPasswordFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInWithEmailAndPassword'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_WRONG_" + "PASSWORD] sign-in with email/password failed'," + " ticker:1}}," + " {fake:'FIRAuth.signInWithEmail:password:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthInvalidCredentialsException:ERROR_WRONG_" + "PASSWORD] sign-in with email/password failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->SignInWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorWrongPassword, result); +} + +TEST_F(AuthTest, TestCreateUserWithEmailAndPasswordFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.createUserWithEmailAndPassword'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthUserCollisionException:ERROR_EMAIL_ALREADY_" + "IN_USE] create user with email/pwd failed'," + " ticker:1}}," + " {fake:'FIRAuth.createUserWithEmail:password:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthUserCollisionException:ERROR_EMAIL_ALREADY_" + "IN_USE] create user with email/pwd failed'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = + firebase_auth_->CreateUserWithEmailAndPassword("abc@xyz.com", "password"); + Verify(kAuthErrorEmailAlreadyInUse, result); +} + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + +TEST_F(AuthTest, TestCurrentUserAndSignOut) { + // Here we let mock sign-in-anonymously succeed immediately (ticker = 0). + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{ticker:0}}," + " {fake:'FIRAuth.FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{ticker:0}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"users\": [{},]}',]" + " }" + " }" + " ]" + "}"); + MakeAuth(); + + // No user is signed in. + EXPECT_EQ(nullptr, firebase_auth_->current_user()); + + // Now sign-in, say anonymously. + Future result = firebase_auth_->SignInAnonymously(); + MaybeWaitForFuture(result); + EXPECT_NE(nullptr, firebase_auth_->current_user()); + + // Now sign-out. + firebase_auth_->SignOut(); + EXPECT_EQ(nullptr, firebase_auth_->current_user()); +} + +TEST_F(AuthTest, TestSendPasswordResetEmailSucceeded) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.sendPasswordResetEmail'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRAuth.sendPasswordResetWithEmail:completion:'," + " futuregeneric:{ticker:1}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{\"email\": \"my@email.com\"}']" + " }" + " }" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SendPasswordResetEmail("my@email.com"); + Verify(kAuthErrorNone, result); +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS +TEST_F(AuthTest, TestSendPasswordResetEmailFailed) { + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.sendPasswordResetEmail'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthEmailException:ERROR_INVALID_MESSAGE_PAYLOAD]" + " failed to send password reset email'," + " ticker:1}}," + " {fake:'FIRAuth.sendPasswordResetWithEmail:completion:'," + " futuregeneric:{throwexception:true," + " " + "exceptionmsg:'[FirebaseAuthEmailException:ERROR_INVALID_MESSAGE_PAYLOAD]" + " failed to send password reset email'," + " ticker:1}}" + " ]" + "}"); + MakeAuth(); + Future result = firebase_auth_->SendPasswordResetEmail("my@email.com"); + Verify(kAuthErrorInvalidMessagePayload, result); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/credential_test.cc b/auth/tests/credential_test.cc new file mode 100644 index 0000000000..f3a0587f73 --- /dev/null +++ b/auth/tests/credential_test.cc @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/credential.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/ticker.h" + +namespace firebase { +namespace auth { + +class CredentialTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + firebase_app_ = testing::CreateApp(); + firebase_auth_ = Auth::GetAuth(firebase_app_); + EXPECT_NE(nullptr, firebase_auth_); + } + + void TearDown() override { + firebase::testing::cppsdk::ConfigReset(); + delete firebase_auth_; + firebase_auth_ = nullptr; + delete firebase_app_; + firebase_app_ = nullptr; + } + + // Helper function to verify the credential result. + void Verify(const Credential& credential, const char* provider) { + EXPECT_TRUE(credential.is_valid()); + EXPECT_EQ(provider, credential.provider()); + } + + App* firebase_app_ = nullptr; + Auth* firebase_auth_ = nullptr; +}; + +TEST_F(CredentialTest, TestEmailAuthProvider) { + // Test get credential from email and password. + Credential credential = EmailAuthProvider::GetCredential("i@email.com", "pw"); + Verify(credential, "password"); +} + +TEST_F(CredentialTest, TestFacebookAuthProvider) { + // Test get credential via Facebook. + Credential credential = FacebookAuthProvider::GetCredential("aFacebookToken"); + Verify(credential, "facebook.com"); +} + +TEST_F(CredentialTest, TestGithubAuthProvider) { + // Test get credential via GitHub. + Credential credential = GitHubAuthProvider::GetCredential("aGitHubToken"); + Verify(credential, "github.com"); +} + +TEST_F(CredentialTest, TestGoogleAuthProvider) { + // Test get credential via Google. + Credential credential = GoogleAuthProvider::GetCredential("red", "blue"); + Verify(credential, "google.com"); +} + +#if defined(__ANDROID__) || defined(FIREBASE_ANDROID_FOR_DESKTOP) +TEST_F(CredentialTest, TestPlayGamesAuthProvider) { + // Test get credential via PlayGames. + Credential credential = PlayGamesAuthProvider::GetCredential("anAuthCode"); + Verify(credential, "playgames.google.com"); +} +#endif // defined(__ANDROID__) || defined(FIREBASE_ANDROID_FOR_DESKTOP) + +TEST_F(CredentialTest, TestTwitterAuthProvider) { + // Test get credential via Twitter. + Credential credential = TwitterAuthProvider::GetCredential("token", "secret"); + Verify(credential, "twitter.com"); +} + +TEST_F(CredentialTest, TestOAuthProvider) { + // Test get credential via OAuth. + Credential credential = OAuthProvider::GetCredential("u.test", "id", "acc"); + Verify(credential, "u.test"); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/auth_desktop_test.cc b/auth/tests/desktop/auth_desktop_test.cc new file mode 100644 index 0000000000..07d746ee1a --- /dev/null +++ b/auth/tests/desktop/auth_desktop_test.cc @@ -0,0 +1,895 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/src/desktop/auth_desktop.h" + +#include +#include +#include +#include + +#include "app/memory/unique_ptr.h" +#include "app/rest/transport_builder.h" +#include "app/rest/transport_curl.h" +#include "app/rest/transport_mock.h" +#include "app/src/include/firebase/app.h" +#include "app/src/mutex.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/desktop/sign_in_flow.h" +#include "auth/src/desktop/user_desktop.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/types.h" +#include "auth/src/include/firebase/auth/user.h" +#include "auth/tests/desktop/fakes.h" +#include "auth/tests/desktop/test_utils.h" +#include "testing/config.h" +#include "testing/ticker.h" +#include "flatbuffers/stl_emulation.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace auth { + +using test::CreateErrorHttpResponse; +using test::FakeSetT; +using test::FakeSuccessfulResponse; +using test::GetFakeOAuthProviderData; +using test::GetUrlForApi; +using test::InitializeConfigWithAFake; +using test::InitializeConfigWithFakes; +using test::OAuthProviderTestHandler; +using test::VerifySignInResult; +using test::WaitForFuture; +using ::testing::IsEmpty; + +namespace { + +const char* const API_KEY = "MY-FAKE-API-KEY"; +// Constant, describing how many times we would like to sleep 1ms to wait +// for loading persistence cache. +const int kWaitForLoadMaxTryout = 500; + +void VerifyProviderData(const User& user) { + const std::vector& provider_data = user.provider_data(); + EXPECT_EQ(1, provider_data.size()); + if (provider_data.empty()) { + return; // Avoid crashing on vector out-of-bounds access below + } + EXPECT_EQ("fake_uid", provider_data[0]->uid()); + EXPECT_EQ("fake_email@example.com", provider_data[0]->email()); + EXPECT_EQ("fake_display_name", provider_data[0]->display_name()); + EXPECT_EQ("fake_photo_url", provider_data[0]->photo_url()); + EXPECT_EQ("fake_provider_id", provider_data[0]->provider_id()); + EXPECT_EQ("123123", provider_data[0]->phone_number()); +} + +void VerifyUser(const User& user) { + EXPECT_EQ("localid123", user.uid()); + EXPECT_EQ("testsignin@example.com", user.email()); + EXPECT_EQ("", user.display_name()); + EXPECT_EQ("", user.photo_url()); + EXPECT_EQ("Firebase", user.provider_id()); + EXPECT_EQ("", user.phone_number()); + EXPECT_FALSE(user.is_email_verified()); + VerifyProviderData(user); +} + +std::string GetFakeProviderInfo() { + return "\"providerUserInfo\": [" + " {" + " \"federatedId\": \"fake_uid\"," + " \"email\": \"fake_email@example.com\"," + " \"displayName\": \"fake_display_name\"," + " \"photoUrl\": \"fake_photo_url\"," + " \"providerId\": \"fake_provider_id\"," + " \"phoneNumber\": \"123123\"" + " }" + "]"; +} + +std::string CreateGetAccountInfoFake() { + return FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\",") + + GetFakeProviderInfo() + + " }" + " ]"); +} + +std::string CreateVerifyAssertionResponse() { + return FakeSuccessfulResponse("VerifyAssertionResponse", + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"idtoken123\"," + "\"providerId\": \"google.com\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); +} + +std::string CreateVerifyAssertionResponseWithUserInfo( + const std::string& provider_id, const std::string& raw_user_info) { + const auto head = std::string( + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"idtoken123\"," + "\"providerId\": \"") + + provider_id + "\","; + + std::string user_info; + if (!raw_user_info.empty()) { + user_info = "\"rawUserInfo\": \"{" + raw_user_info + "}\","; + } + + const auto tail = + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""; + + const auto body = head + user_info + tail; + return FakeSuccessfulResponse("VerifyAssertionResponse", body); +} + +void InitializeSignInWithProviderFakes( + const std::string& get_account_info_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = get_account_info_response; + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulSignInWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + const std::string& get_account_info_response) { + InitializeSignInWithProviderFakes(get_account_info_response); + provider->SetProviderData(GetFakeOAuthProviderData()); + provider->SetAuthHandler(handler); +} + +void InitializeSuccessfulSignInWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler) { + InitializeSuccessfulSignInWithProviderFlow(provider, handler, + CreateGetAccountInfoFake()); +} + +void InitializeSuccessfulVerifyAssertionFlow( + const std::string& verify_assertion_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = verify_assertion_response; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulVerifyAssertionFlow() { + InitializeSuccessfulVerifyAssertionFlow(CreateVerifyAssertionResponse()); +} + +void SetupAuthDataForPersist(AuthData* auth_data) { + UserData previous_user; + UserData mock_user; + + mock_user.uid = "persist_id"; + mock_user.email = "test@persist.com"; + mock_user.display_name = "persist_name"; + mock_user.photo_url = "persist_photo"; + mock_user.provider_id = "persist_provider"; + mock_user.phone_number = "persist_phone"; + mock_user.is_anonymous = false; + mock_user.is_email_verified = true; + mock_user.id_token = "persist_token"; + mock_user.refresh_token = "persist_refresh_token"; + mock_user.access_token = "persist_access_token"; + mock_user.access_token_expiration_date = 12345; + mock_user.has_email_password_credential = true; + mock_user.last_sign_in_timestamp = 67890; + mock_user.creation_timestamp = 98765; + UserView::ResetUser(auth_data, mock_user, &previous_user); +} + +bool WaitOnLoadPersistence(AuthData* auth_data) { + bool load_finished = false; + int load_wait_counter = 0; + while (!load_finished) { + if (load_wait_counter >= kWaitForLoadMaxTryout) { + break; + } + load_wait_counter++; + firebase::internal::Sleep(1); + { + MutexLock lock(auth_data->listeners_mutex); + load_finished = !auth_data->persistent_cache_load_pending; + } + } + return load_finished; +} + +} // namespace + +class AuthDesktopTest : public ::testing::Test { + protected: + void SetUp() override { + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); + AppOptions options = testing::MockAppOptions(); + options.set_app_id("com.firebase.test"); + options.set_api_key(API_KEY); + firebase_app_ = std::unique_ptr(App::Create(options)); + firebase_auth_ = std::unique_ptr(Auth::GetAuth(firebase_app_.get())); + EXPECT_NE(nullptr, firebase_auth_); + + firebase_auth_->AddIdTokenListener(&id_token_listener); + firebase_auth_->AddAuthStateListener(&auth_state_listener); + + WaitOnLoadPersistence(firebase_auth_->auth_data_); + } + + void TearDown() override { + // Reset listeners before signing out. + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + firebase_auth_->SignOut(); + firebase_auth_.reset(nullptr); + firebase_app_.reset(nullptr); + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + Future ProcessSignInWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + bool trigger_sign_in) { + InitializeSignInWithProviderFakes(CreateGetAccountInfoFake()); + provider->SetProviderData(GetFakeOAuthProviderData()); + provider->SetAuthHandler(handler); + Future future = firebase_auth_->SignInWithProvider(provider); + if (trigger_sign_in) { + handler->TriggerSignInComplete(); + } + return future; + } + + std::unique_ptr firebase_app_; + std::unique_ptr firebase_auth_; + + test::IdTokenChangesCounter id_token_listener; + test::AuthStateChangesCounter auth_state_listener; +}; + +TEST_F(AuthDesktopTest, + TestSignInWithProviderReturnsUnsupportedError) { + FederatedOAuthProvider provider; + Future future = firebase_auth_->SignInWithProvider(&provider); + EXPECT_EQ(future.result()->user, nullptr); + EXPECT_EQ(future.error(), kAuthErrorUnimplemented); + EXPECT_EQ(std::string(future.error_message()), + "Operation is not supported on non-mobile systems."); +} + +TEST_F(AuthDesktopTest, + DISABLED_TestSignInWithProviderAndHandlerPassingIntegrityChecks) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler(/*extra_integrity_checks_=*/true); + + InitializeSuccessfulSignInWithProviderFlow(&provider, &handler); + Future future = firebase_auth_->SignInWithProvider(&provider); + handler.TriggerSignInComplete(); + SignInResult sign_in_result = WaitForFuture(future); +} + +TEST_F(AuthDesktopTest, + DISABLED_TestPendingSignInWithProviderSecondConcurrentSignInFails) { + FederatedOAuthProvider provider1; + OAuthProviderTestHandler handler1; + InitializeSuccessfulSignInWithProviderFlow(&provider1, &handler1); + + FederatedOAuthProvider provider2; + provider2.SetProviderData(GetFakeOAuthProviderData()); + + OAuthProviderTestHandler handler2; + provider2.SetAuthHandler(&handler2); + Future future1 = firebase_auth_->SignInWithProvider(&provider1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = firebase_auth_->SignInWithProvider(&provider2); + VerifySignInResult(future2, kAuthErrorFederatedProviderAreadyInUse); + handler1.TriggerSignInComplete(); + const SignInResult sign_in_result = WaitForFuture(future1); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteSignInResultUserPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + FederatedAuthProvider::AuthenticatedUserData user_data = + *(handler.GetAuthenticatedUserData()); + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + EXPECT_NE(sign_in_result.user, nullptr); + EXPECT_EQ(sign_in_result.user->is_email_verified(), + user_data.is_email_verified); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + EXPECT_EQ(sign_in_result.user->uid(), user_data.uid); + EXPECT_EQ(sign_in_result.user->email(), user_data.email); + EXPECT_EQ(sign_in_result.user->display_name(), user_data.display_name); + EXPECT_EQ(sign_in_result.user->photo_url(), user_data.photo_url); + EXPECT_EQ(sign_in_result.user->provider_id(), user_data.provider_id); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullUIDFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->uid = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullDisplayNamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->display_name = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullUsernamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->user_name = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullPhotoUrlPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->photo_url = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullProvderIdFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->provider_id = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullAccessTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->access_token = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullRefreshTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->refresh_token = nullptr; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteExpiresInMaxUInt64Passes) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->token_expires_in_seconds = ULONG_MAX; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteErrorMessagePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/false); + const char* error_message = "oh nos!"; + handler.TriggerSignInCompleteWithError(kAuthErrorApiNotAvailable, + error_message); + VerifySignInResult(future, kAuthErrorApiNotAvailable, error_message); +} + +TEST_F(AuthDesktopTest, DISABLED_TestSignInCompleteNullErrorMessageFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessSignInWithProviderFlow( + &provider, &handler, /*trigger_sign_in=*/false); + handler.TriggerSignInCompleteWithError(kAuthErrorApiNotAvailable, nullptr); + VerifySignInResult(future, kAuthErrorApiNotAvailable); +} + +// Test the helper function GetAccountInfo. +TEST_F(AuthDesktopTest, TestGetAccountInfo) { + const auto response = + FakeSuccessfulResponse("GetAccountInfoResponse", + "\"users\": " + " [" + " {" + " \"localId\": \"localid123\"," + " \"displayName\": \"dp name\"," + " \"email\": \"abc@efg\"," + " \"photoUrl\": \"www.photo\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"phoneNumber\": \"519\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\"" + " }" + " ]"); + InitializeConfigWithAFake(GetUrlForApi("APIKEY", "getAccountInfo"), response); + + // getAccountInfo never returns new tokens, and can't change current user. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Call the function and verify results. + AuthData auth_data; + AuthImpl auth; + auth_data.auth_impl = &auth; + auth.api_key = "APIKEY"; + const GetAccountInfoResult result = + GetAccountInfo(auth_data, "fake_access_token"); + EXPECT_TRUE(result.IsValid()); + const UserData& user = result.user(); + EXPECT_EQ("localid123", user.uid); + EXPECT_EQ("abc@efg", user.email); + EXPECT_EQ("dp name", user.display_name); + EXPECT_EQ("www.photo", user.photo_url); + EXPECT_EQ("519", user.phone_number); + EXPECT_FALSE(user.is_email_verified); + EXPECT_TRUE(user.has_email_password_credential); +} + +// Test the helper function CompleteSignIn. Since we do not have the access to +// the private members of Auth, we use SignInAnonymously to test it indirectly. +// Let the REST request failed with 503. +TEST_F(AuthDesktopTest, CompleteSignInWithFailedResponse) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = CreateErrorHttpResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + // Because the API call fails, current user shouldn't have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Call the function and verify results. + const User* const user = + WaitForFuture(firebase_auth_->SignInAnonymously(), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +// Test the helper function CompleteSignIn. Since we do not have the access to +// the private members of Auth, we use SignInAnonymously to test it indirectly. +// Let it failed to get account info. +TEST_F(AuthDesktopTest, CompleteSignInWithGetAccountInfoFailure) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + "\"idToken\": \"idtoken123\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\"," + "\"localId\": \"localid123\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateErrorHttpResponse(); + InitializeConfigWithFakes(fakes); + + // User is not updated until getAccountInfo succeeds; calls to signupNewUser + // and getAccountInfo are considered a single "transaction". So if + // getAccountInfo fails, current user shouldn't have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Call the function and verify results. + const User* const user = + WaitForFuture(firebase_auth_->SignInAnonymously(), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +// Test Auth::SignInAnonymously. +TEST_F(AuthDesktopTest, TestSignInAnonymously) { + FakeSetT fakes; + + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + "\"idToken\": \"idtoken123\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\"," + "\"localId\": \"localid123\""); + + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + FakeSuccessfulResponse("GetAccountInfoResponse", + "\"users\": " + " [" + " {" + " \"localId\": \"localid123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\"" + " }" + " ]"); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const User* const user = WaitForFuture(firebase_auth_->SignInAnonymously()); + EXPECT_TRUE(user->is_anonymous()); + EXPECT_EQ("localid123", user->uid()); + EXPECT_EQ("", user->email()); + EXPECT_EQ("", user->display_name()); + EXPECT_EQ("", user->photo_url()); + EXPECT_EQ("Firebase", user->provider_id()); + EXPECT_EQ("", user->phone_number()); + EXPECT_FALSE(user->is_email_verified()); +} + +// Test Auth::SignInWithEmailAndPassword. +TEST_F(AuthDesktopTest, TestSignInWithEmailAndPassword) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyPassword")] = + FakeSuccessfulResponse("VerifyPasswordResponse", + "\"localId\": \"localid123\"," + "\"email\": \"testsignin@example.com\"," + "\"idToken\": \"idtoken123\"," + "\"registered\": true," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + // Call the function and verify results. + const Future future = firebase_auth_->SignInWithEmailAndPassword( + "testsignin@example.com", "testsignin"); + const User* const user = WaitForFuture(future); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +// Test Auth::CreateUserWithEmailAndPassword. +TEST_F(AuthDesktopTest, TestCreateUserWithEmailAndPassword) { + FakeSetT fakes; + + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + "\"idToken\": \"idtoken123\"," + "\"displayName\": \"\"," + "\"email\": \"testsignin@example.com\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\"," + "\"localId\": \"localid123\""); + + fakes[GetUrlForApi(API_KEY, "verifyPassword")] = + FakeSuccessfulResponse("VerifyPasswordResponse", + "\"localId\": \"localid123\"," + "\"email\": \"testsignin@example.com\"," + "\"idToken\": \"idtoken123\"," + "\"registered\": true," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); + + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Future future = firebase_auth_->CreateUserWithEmailAndPassword( + "testsignin@example.com", "testsignin"); + const User* const user = WaitForFuture(future); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +// Test Auth::SignInWithCustomToken. +TEST_F(AuthDesktopTest, TestSignInWithCustomToken) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyCustomToken")] = + FakeSuccessfulResponse("VerifyCustomTokenResponse", + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"idtoken123\"," + "\"refreshToken\": \"refreshtoken123\"," + "\"expiresIn\": \"3600\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const User* const user = + WaitForFuture(firebase_auth_->SignInWithCustomToken("fake_custom_token")); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +// Test Auth::TestSignInWithCredential. + +TEST_F(AuthDesktopTest, TestSignInWithCredential_GoogleIdToken) { + InitializeSuccessfulVerifyAssertionFlow(); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const User* const user = + WaitForFuture(firebase_auth_->SignInWithCredential(credential)); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +TEST_F(AuthDesktopTest, TestSignInWithCredential_GoogleAccessToken) { + InitializeSuccessfulVerifyAssertionFlow(); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = + GoogleAuthProvider::GetCredential("", "fake_access_token"); + const User* const user = + WaitForFuture(firebase_auth_->SignInWithCredential(credential)); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +TEST_F(AuthDesktopTest, + TestSignInWithCredential_WithFailedVerifyAssertionResponse) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = CreateErrorHttpResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateGetAccountInfoFake(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Credential credential = + GoogleAuthProvider::GetCredential("", "fake_access_token"); + const User* const user = WaitForFuture( + firebase_auth_->SignInWithCredential(credential), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +TEST_F(AuthDesktopTest, + TestSignInWithCredential_WithFailedGetAccountInfoResponse) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = + CreateVerifyAssertionResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = CreateErrorHttpResponse(); + InitializeConfigWithFakes(fakes); + + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Credential credential = + GoogleAuthProvider::GetCredential("", "fake_access_token"); + const User* const user = WaitForFuture( + firebase_auth_->SignInWithCredential(credential), kAuthErrorFailure); + EXPECT_EQ(nullptr, user); +} + +TEST_F(AuthDesktopTest, TestSignInWithCredential_NeedsConfirmation) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "verifyAssertion"), + FakeSuccessfulResponse("verifyAssertion", "\"needConfirmation\": true")); + + // needConfirmation is considered an error by the SDK, so current user + // shouldn't have been updated. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_auth_->SignInWithCredential(credential), + kAuthErrorAccountExistsWithDifferentCredentials); +} + +TEST_F(AuthDesktopTest, TestSignInAndRetrieveDataWithCredential_GitHub) { + const auto response = CreateVerifyAssertionResponseWithUserInfo( + "github.com", + "\\\\\"login\\\\\": \\\\\"fake_user_name\\\\\"," + "\\\\\"some_str_key\\\\\": \\\\\"some_value\\\\\"," + "\\\\\"some_num_key\\\\\": 123"); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = + GitHubAuthProvider::GetCredential("fake_access_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_STREQ("github.com", sign_in_result.info.provider_id.c_str()); + EXPECT_STREQ("fake_user_name", sign_in_result.info.user_name.c_str()); + + const auto found_str_value = + sign_in_result.info.profile.find(Variant("some_str_key")); + EXPECT_NE(found_str_value, sign_in_result.info.profile.end()); + EXPECT_STREQ("some_value", found_str_value->second.string_value()); + + const auto found_num_value = + sign_in_result.info.profile.find(Variant("some_num_key")); + EXPECT_NE(found_num_value, sign_in_result.info.profile.end()); + EXPECT_EQ(123, found_num_value->second.int64_value()); +} + +TEST_F(AuthDesktopTest, TestSignInAndRetrieveDataWithCredential_Twitter) { + const auto response = CreateVerifyAssertionResponseWithUserInfo( + "twitter.com", "\\\\\"screen_name\\\\\": \\\\\"fake_user_name\\\\\""); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = TwitterAuthProvider::GetCredential( + "fake_access_token", "fake_oauth_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_EQ("twitter.com", sign_in_result.info.provider_id); + EXPECT_EQ("fake_user_name", sign_in_result.info.user_name); +} + +TEST_F(AuthDesktopTest, + TestSignInAndRetrieveDataWithCredential_NoAdditionalInfo) { + const auto response = + CreateVerifyAssertionResponseWithUserInfo("github.com", ""); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = TwitterAuthProvider::GetCredential( + "fake_access_token", "fake_oauth_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_EQ("github.com", sign_in_result.info.provider_id); + EXPECT_THAT(sign_in_result.info.profile, IsEmpty()); + EXPECT_THAT(sign_in_result.info.user_name, IsEmpty()); +} + +TEST_F(AuthDesktopTest, + TestSignInAndRetrieveDataWithCredential_BadUserNameFormat) { + // Deliberately using a number instead of a string, let's make sure it doesn't + // cause a crash. + const auto response = CreateVerifyAssertionResponseWithUserInfo( + "twitter.com", "\\\\\"screen_name\\\\\": 123"); + InitializeSuccessfulVerifyAssertionFlow(response); + + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + const Credential credential = TwitterAuthProvider::GetCredential( + "fake_access_token", "fake_oauth_token"); + const SignInResult sign_in_result = WaitForFuture( + firebase_auth_->SignInAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); + + EXPECT_EQ("twitter.com", sign_in_result.info.provider_id); + EXPECT_THAT(sign_in_result.info.user_name, IsEmpty()); +} + +TEST_F(AuthDesktopTest, TestFetchProvidersForEmail) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "createAuthUri"), + FakeSuccessfulResponse("CreateAuthUriResponse", + "\"allProviders\": [" + " \"password\"," + " \"example.com\"" + "]," + "\"registered\": true")); + + // Fetch providers flow shouldn't affect current user in any way. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + const Auth::FetchProvidersResult result = WaitForFuture( + firebase_auth_->FetchProvidersForEmail("fake_email@example.com")); + EXPECT_EQ(2, result.providers.size()); + EXPECT_EQ("password", result.providers[0]); + EXPECT_EQ("example.com", result.providers[1]); +} + +TEST_F(AuthDesktopTest, TestSendPasswordResetEmail) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "getOobConfirmationCode"), + FakeSuccessfulResponse("GetOobConfirmationCodeResponse", + "\"email\": \"fake_email@example.com\"")); + + // Sending password reset email shouldn't affect current user in any way. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + WaitForFuture( + firebase_auth_->SendPasswordResetEmail("fake_email@example.com")); +} + +TEST(UserViewTest, TestCopyUserView) { + // Construct from UserData. + UserData user1; + user1.uid = "mrsspoon"; + UserView view1(user1); + UserView view3(view1); + UserView view4 = view3; + EXPECT_EQ("mrsspoon", view1.user_data().uid); + EXPECT_EQ("mrsspoon", view3.user_data().uid); + EXPECT_EQ("mrsspoon", view4.user_data().uid); + + // Construct from a UserView. + UserData user2; + user2.uid = "dangerm"; + UserView view2(user2); + EXPECT_EQ("dangerm", view2.user_data().uid); + + // Copy a UserView. + view3 = view2; + EXPECT_EQ("mrsspoon", view1.user_data().uid); + EXPECT_EQ("dangerm", view2.user_data().uid); + EXPECT_EQ("dangerm", view3.user_data().uid); +} + +#if defined(FIREBASE_USE_MOVE_OPERATORS) +TEST(UserViewTest, TestMoveUserView) { + UserData user1; + user1.uid = "mrsspoon"; + UserData user2; + user2.uid = "dangerm"; + UserView view1(user1); + UserView view2(user2); + UserView view3(user2); + UserView view4(std::move(view3)); + EXPECT_EQ("mrsspoon", view1.user_data().uid); + EXPECT_EQ("dangerm", view2.user_data().uid); + EXPECT_EQ("dangerm", view4.user_data().uid); + view2 = std::move(view1); + EXPECT_EQ("mrsspoon", view2.user_data().uid); +} +#endif // defined(defined(FIREBASE_USE_MOVE_OPERATORS) + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/fakes.cc b/auth/tests/desktop/fakes.cc new file mode 100644 index 0000000000..348f97838a --- /dev/null +++ b/auth/tests/desktop/fakes.cc @@ -0,0 +1,118 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/tests/desktop/fakes.h" + +#include "testing/config.h" + +namespace firebase { +namespace auth { +namespace test { + +std::string CreateRawJson(const FakeSetT& fakes) { + std::string raw_json = + "{" + " config:" + " ["; + + for (auto i = fakes.begin(); i != fakes.end(); ++i) { + const std::string url = i->first; + const std::string response = i->second; + raw_json += + " {" + " fake: '" + + url + + "'," + " httpresponse: " + + response + " }"; + auto check_end = i; + ++check_end; + if (check_end != fakes.end()) { + raw_json += ','; + } + } + + raw_json += + " ]" + "}"; + + return raw_json; +} + +void InitializeConfigWithFakes(const FakeSetT& fakes) { + firebase::testing::cppsdk::ConfigSet(CreateRawJson(fakes).c_str()); +} + +void InitializeConfigWithAFake(const std::string& url, + const std::string& fake_response) { + FakeSetT fakes; + fakes[url] = fake_response; + InitializeConfigWithFakes(fakes); +} + +std::string GetUrlForApi(const std::string& api_key, + const std::string& api_method) { + const char* const base_url = + "https://www.googleapis.com/identitytoolkit/v3/" + "relyingparty/"; + return std::string{base_url} + api_method + "?key=" + api_key; +} + +std::string FakeSuccessfulResponse(const std::string& body) { + const std::string head = + "{" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: " + " [" + " '{"; + + const std::string tail = + " }'" + " ]" + "}"; + + return head + body + tail; +} + +std::string FakeSuccessfulResponse(const std::string& kind, + const std::string& body) { + return FakeSuccessfulResponse("\"kind\": \"identitytoolkit#" + kind + "\"," + + body); +} + +std::string CreateErrorHttpResponse(const std::string& error) { + const std::string head = + "{" + " header: ['HTTP/1.1 503 Service Unavailable','Server:mock 101']"; + + std::string body; + if (!error.empty()) { + // clang-format off + body = std::string( + "," + " body: ['{" + " \"error\": {" + " \"message\": \"") + error + "\"" + " }" + " }']"; + // clang-format on + } + + const std::string tail = "}"; + return head + body + tail; +} + +} // namespace test +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/fakes.h b/auth/tests/desktop/fakes.h new file mode 100644 index 0000000000..e84d9ed80c --- /dev/null +++ b/auth/tests/desktop/fakes.h @@ -0,0 +1,64 @@ +// Copyright 2017 Google LLC +// +// 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. + +#ifndef FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_FAKES_H_ +#define FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_FAKES_H_ + +#include +#include + +// A set of helpers to reduce repetitive boilerplate to setup fakes in tests. + +namespace firebase { +namespace auth { +namespace test { + +using FakeSetT = std::unordered_map; + +// Creates a JSON string from the given map of fakes (which assumes a very +// simple format, both keys and values can only be strings). +std::string CreateRawJson(const FakeSetT& fakes); + +// Creates a JSON string from the given map of fakes and initializes Firebase +// testing config with this JSON. +void InitializeConfigWithFakes(const FakeSetT& fakes); + +// Creates JSON dictionary with just a single entry (key = url, value +// = fake_response) and initializes Firebase testing config with this JSON. +void InitializeConfigWithAFake(const std::string& url, + const std::string& fake_response); + +// Returns full URL to make a REST request to Identity Toolkit backend. +std::string GetUrlForApi(const std::string& api_key, + const std::string& api_method); + +// Returns string representation of a successful HTTP response with the given +// body. +std::string FakeSuccessfulResponse(const std::string& body); + +// Returns string representation of a successful HTTP response with the given +// body. Body will also contain an entry to specify the "kind" of response, like +// all Identity Toolkit responses do ("kind": +// "identitytoolkit#"). +std::string FakeSuccessfulResponse(const std::string& kind, + const std::string& body); + +// Returns string representation of a 503 HTTP response. +std::string CreateErrorHttpResponse(const std::string& error = ""); + +} // namespace test +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_FAKES_H_ diff --git a/auth/tests/desktop/rpcs/create_auth_uri_test.cc b/auth/tests/desktop/rpcs/create_auth_uri_test.cc new file mode 100644 index 0000000000..92260d615f --- /dev/null +++ b/auth/tests/desktop/rpcs/create_auth_uri_test.cc @@ -0,0 +1,66 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/create_auth_uri_request.h" +#include "auth/src/desktop/rpcs/create_auth_uri_response.h" + +namespace firebase { +namespace auth { + +// Test CreateAuthUriRequest +TEST(CreateAuthUriTest, TestCreateAuthUriRequest) { + std::unique_ptr app(testing::CreateApp()); + CreateAuthUriRequest request("APIKEY", "email"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "createAuthUri?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " identifier: \"email\",\n" + " continueUri: \"http://localhost\"\n" + "}\n", + request.options().post_fields); +} + +// Test CreateAuthUriResponse +TEST(CreateAuthUriTest, TestCreateAuthUriResponse) { + std::unique_ptr app(testing::CreateApp()); + CreateAuthUriResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#CreateAuthUriResponse\",\n" + " \"allProviders\": [\n" + " \"password\"\n" + " ],\n" + " \"registered\": true,\n" + " \"sessionId\": \"cdefgab\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_THAT(response.providers(), ::testing::ElementsAre("password")); + EXPECT_TRUE(response.registered()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/delete_account_test.cc b/auth/tests/desktop/rpcs/delete_account_test.cc new file mode 100644 index 0000000000..7240f31319 --- /dev/null +++ b/auth/tests/desktop/rpcs/delete_account_test.cc @@ -0,0 +1,57 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/delete_account_request.h" +#include "auth/src/desktop/rpcs/delete_account_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test DeleteAccountRequest +TEST(DeleteAccountTest, TestDeleteAccountRequest) { + std::unique_ptr app(testing::CreateApp()); + DeleteAccountRequest request("APIKEY"); + request.SetIdToken("token"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "deleteAccount?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " idToken: \"token\"\n" + "}\n", + request.options().post_fields); +} + +// Test DeleteAccountResponse +TEST(DeleteAccountTest, TestDeleteAccountResponse) { + std::unique_ptr app(testing::CreateApp()); + DeleteAccountResponse response; + const char body[] = + "{\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/get_account_info_test.cc b/auth/tests/desktop/rpcs/get_account_info_test.cc new file mode 100644 index 0000000000..cd4f241c9a --- /dev/null +++ b/auth/tests/desktop/rpcs/get_account_info_test.cc @@ -0,0 +1,81 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/get_account_info_request.h" +#include "auth/src/desktop/rpcs/get_account_info_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test GetAccountInfoRequest +TEST(GetAccountInfoTest, TestGetAccountInfoRequest) { + std::unique_ptr app(testing::CreateApp()); + GetAccountInfoRequest request("APIKEY", "token"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " idToken: \"token\"\n" + "}\n", + request.options().post_fields); +} + +// Test GetAccountInfoResponse +TEST(GetAccountInfoTest, TestGetAccountInfoResponse) { + std::unique_ptr app(App::Create(AppOptions())); + GetAccountInfoResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#GetAccountInfoResponse\",\n" + " \"users\": [\n" + " {\n" + " \"localId\": \"localid123\",\n" + " \"displayName\": \"dp name\",\n" + " \"email\": \"abc@efg\",\n" + " \"photoUrl\": \"www.photo\",\n" + " \"emailVerified\": false,\n" + " \"passwordHash\": \"abcdefg\",\n" + " \"phoneNumber\": \"519\",\n" + " \"passwordUpdatedAt\": 31415926,\n" + " \"validSince\": \"123\",\n" + " \"lastLoginAt\": \"123\",\n" + " \"createdAt\": \"123\"\n" + " }\n" + " ]\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("localid123", response.local_id()); + EXPECT_EQ("dp name", response.display_name()); + EXPECT_EQ("abc@efg", response.email()); + EXPECT_EQ("www.photo", response.photo_url()); + EXPECT_FALSE(response.email_verified()); + EXPECT_EQ("abcdefg", response.password_hash()); + EXPECT_EQ("519", response.phone_number()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc b/auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc new file mode 100644 index 0000000000..53d465275a --- /dev/null +++ b/auth/tests/desktop/rpcs/get_oob_confirmation_code_test.cc @@ -0,0 +1,81 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/get_oob_confirmation_code_request.h" +#include "auth/src/desktop/rpcs/get_oob_confirmation_code_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +typedef GetOobConfirmationCodeRequest RequestT; +typedef GetOobConfirmationCodeResponse ResponseT; + +// Test SetVerifyEmailRequest +TEST(GetOobConfirmationCodeTest, SendVerifyEmailRequest) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateSendEmailVerificationRequest("APIKEY"); + request->SetIdToken("token"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " idToken: \"token\",\n" + " requestType: \"VERIFY_EMAIL\"\n" + "}\n", + request->options().post_fields); +} + +TEST(GetOobConfirmationCodeTest, SendPasswordResetEmailRequest) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateSendPasswordResetEmailRequest("APIKEY", "email"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " email: \"email\",\n" + " requestType: \"PASSWORD_RESET\"\n" + "}\n", + request->options().post_fields); +} + +// Test GetOobConfirmationCodeResponse +TEST(GetOobConfirmationCodeTest, TestGetOobConfirmationCodeResponse) { + std::unique_ptr app(testing::CreateApp()); + ResponseT response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#GetOobConfirmationCodeResponse\",\n" + " \"email\": \"my@email\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/reset_password_test.cc b/auth/tests/desktop/rpcs/reset_password_test.cc new file mode 100644 index 0000000000..5e45de3a22 --- /dev/null +++ b/auth/tests/desktop/rpcs/reset_password_test.cc @@ -0,0 +1,61 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/reset_password_request.h" +#include "auth/src/desktop/rpcs/reset_password_response.h" + +namespace firebase { +namespace auth { + +// Test ResetPasswordRequest +TEST(ResetPasswordTest, TestResetPasswordRequest) { + std::unique_ptr app(testing::CreateApp()); + ResetPasswordRequest request("APIKEY", "oob", "password"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "resetPassword?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " oobCode: \"oob\",\n" + " newPassword: \"password\"\n" + "}\n", + request.options().post_fields); +} + +// Test ResetPasswordResponse +TEST(ResetPasswordTest, TestResetPasswordResponse) { + std::unique_ptr app(testing::CreateApp()); + ResetPasswordResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#ResetPasswordResponse\",\n" + " \"email\": \"abc@email\",\n" + " \"requestType\": \"PASSWORD_RESET\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/secure_token_test.cc b/auth/tests/desktop/rpcs/secure_token_test.cc new file mode 100644 index 0000000000..0a8b552c97 --- /dev/null +++ b/auth/tests/desktop/rpcs/secure_token_test.cc @@ -0,0 +1,68 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/secure_token_request.h" +#include "auth/src/desktop/rpcs/secure_token_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test SignUpNewUserRequest using refresh token +TEST(SecureTokenTest, TestSetRefreshRequest) { + std::unique_ptr app(testing::CreateApp()); + SecureTokenRequest request("APIKEY", "token123"); + EXPECT_EQ("https://securetoken.googleapis.com/v1/token?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " grantType: \"refresh_token\",\n" + " refreshToken: \"token123\"\n" + "}\n", + request.options().post_fields); +} + +// Test SecureTokenResponse +TEST(SecureTokenTest, TestSecureTokenResponse) { + std::unique_ptr app(testing::CreateApp()); + SecureTokenResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"access_token\": \"accesstoken123\",\n" + " \"expires_in\": \"3600\",\n" + " \"token_type\": \"Bearer\",\n" + " \"refresh_token\": \"refreshtoken123\",\n" + " \"id_token\": \"idtoken123\",\n" + " \"user_id\": \"localid123\",\n" + " \"project_id\": \"53101460582\"" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("accesstoken123", response.access_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ(3600, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/set_account_info_test.cc b/auth/tests/desktop/rpcs/set_account_info_test.cc new file mode 100644 index 0000000000..622f427db7 --- /dev/null +++ b/auth/tests/desktop/rpcs/set_account_info_test.cc @@ -0,0 +1,173 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/set_account_info_request.h" +#include "auth/src/desktop/rpcs/set_account_info_response.h" + +namespace firebase { +namespace auth { + +typedef SetAccountInfoRequest RequestT; +typedef SetAccountInfoResponse ResponseT; + +// Test SetAccountInfoRequest +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateEmail) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateUpdateEmailRequest("APIKEY", "fakeemail"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " email: \"fakeemail\",\n" + " returnSecureToken: true,\n" + " idToken: \"token\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdatePassword) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUpdatePasswordRequest("APIKEY", "fakepassword"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " password: \"fakepassword\",\n" + " returnSecureToken: true,\n" + " idToken: \"token\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateProfile_Full) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUpdateProfileRequest("APIKEY", "New Name", "new_url"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " displayName: \"New Name\",\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " photoUrl: \"new_url\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateProfile_Partial) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUpdateProfileRequest("APIKEY", nullptr, "new_url"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " photoUrl: \"new_url\"\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_UpdateProfile_DeleteFields) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateUpdateProfileRequest("APIKEY", "", ""); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " deleteAttribute: [\n" + " \"DISPLAY_NAME\",\n" + " \"PHOTO_URL\"\n" + " ]\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, + TestSetAccountInfoRequest_UpdateProfile_DeleteAndUpdate) { + std::unique_ptr app(testing::CreateApp()); + auto request = RequestT::CreateUpdateProfileRequest("APIKEY", "", "new_url"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " photoUrl: \"new_url\",\n" + " deleteAttribute: [\n" + " \"DISPLAY_NAME\"\n" + " ]\n" + "}\n", + request->options().post_fields); +} + +TEST(SetAccountInfoTest, TestSetAccountInfoRequest_Unlink) { + std::unique_ptr app(testing::CreateApp()); + auto request = + RequestT::CreateUnlinkProviderRequest("APIKEY", "fakeprovider"); + request->SetIdToken("token"); + + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=APIKEY", + request->options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " idToken: \"token\",\n" + " deleteProvider: [\n" + " \"fakeprovider\"\n" + " ]\n" + "}\n", + request->options().post_fields); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/sign_up_new_user_test.cc b/auth/tests/desktop/rpcs/sign_up_new_user_test.cc new file mode 100644 index 0000000000..8020349821 --- /dev/null +++ b/auth/tests/desktop/rpcs/sign_up_new_user_test.cc @@ -0,0 +1,110 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_request.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_response.h" + +namespace firebase { +namespace auth { + +// Test SignUpNewUserRequest for making anonymous signin +TEST(SignUpNewUserTest, TestAnonymousSignInRequest) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserRequest request("APIKEY"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true\n" + "}\n", + request.options().post_fields); +} + +// Test SignUpNewUserRequest for using password signin +TEST(SignUpNewUserTest, TestEmailPasswordSignInRequest) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserRequest request("APIKEY", "e@mail", "pwd", "rabbit"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " email: \"e@mail\",\n" + " password: \"pwd\",\n" + " displayName: \"rabbit\",\n" + " returnSecureToken: true\n" + "}\n", + request.options().post_fields); +} + +// Test SignUpNewUserResponse +TEST(SignUpNewUserTest, TestSignUpNewUserResponse) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#SignupNewUserResponse\",\n" + " \"idToken\": \"idtoken123\",\n" + " \"refreshToken\": \"refreshtoken123\",\n" + " \"expiresIn\": \"3600\",\n" + " \"localId\": \"localid123\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); + EXPECT_EQ(3600, response.expires_in()); +} + +TEST(SignUpNewUserTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + SignUpNewUserResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"OPERATION_NOT_ALLOWED\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorOperationNotAllowed, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ(0, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/test_util.cc b/auth/tests/desktop/rpcs/test_util.cc new file mode 100644 index 0000000000..16caf880b2 --- /dev/null +++ b/auth/tests/desktop/rpcs/test_util.cc @@ -0,0 +1,69 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_request.h" +#include "auth/src/desktop/rpcs/sign_up_new_user_response.h" + +namespace firebase { +namespace auth { + +bool GetNewUserLocalIdAndIdToken(const char* const api_key, + std::string* local_id, + std::string* id_token) { + SignUpNewUserRequest request(api_key); + SignUpNewUserResponse response; + + firebase::rest::CreateTransport()->Perform(request, &response); + + if (response.status() != 200) { + return false; + } + + *local_id = response.local_id(); + *id_token = response.id_token(); + return true; +} + +bool GetNewUserLocalIdAndRefreshToken(const char* const api_key, + std::string* local_id, + std::string* refresh_token) { + SignUpNewUserRequest request(api_key); + SignUpNewUserResponse response; + + firebase::rest::CreateTransport()->Perform(request, &response); + + if (response.status() != 200) { + return false; + } + + *local_id = response.local_id(); + *refresh_token = response.refresh_token(); + return true; +} + +std::string SignUpNewUserAndGetIdToken(const char* const api_key, + const char* const email) { + SignUpNewUserRequest request(api_key, email, "fake_password", ""); + SignUpNewUserResponse response; + + firebase::rest::CreateTransport()->Perform(request, &response); + if (response.status() != 200) { + return ""; + } + return response.id_token(); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/test_util.h b/auth/tests/desktop/rpcs/test_util.h new file mode 100644 index 0000000000..62abe172f1 --- /dev/null +++ b/auth/tests/desktop/rpcs/test_util.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#ifndef FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_RPCS_TEST_UTIL_H_ +#define FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_RPCS_TEST_UTIL_H_ + +#include + +namespace firebase { +namespace auth { + +// Sign in a new user and return its local ID and ID token. +bool GetNewUserLocalIdAndIdToken(const char* api_key, std::string* local_id, + std::string* id_token); + +// Sign in a new user and return its local ID and refresh token. +bool GetNewUserLocalIdAndRefreshToken(const char* api_key, + std::string* local_id, + std::string* refresh_token); +std::string SignUpNewUserAndGetIdToken(const char* api_key, + const char* email); + +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_RPCS_TEST_UTIL_H_ diff --git a/auth/tests/desktop/rpcs/verify_assertion_test.cc b/auth/tests/desktop/rpcs/verify_assertion_test.cc new file mode 100644 index 0000000000..e4dc6bc73e --- /dev/null +++ b/auth/tests/desktop/rpcs/verify_assertion_test.cc @@ -0,0 +1,87 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/verify_assertion_request.h" +#include "auth/src/desktop/rpcs/verify_assertion_response.h" + +namespace { +void CheckUrl(const firebase::auth::VerifyAssertionRequest& request) { + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyAssertion?key=APIKEY", + request.options().url); +} +} // namespace + +namespace firebase { +namespace auth { + +// Test VerifyAssertionRequest +TEST(VerifyAssertionTest, TestVerifyAssertionRequest_FromIdToken) { + std::unique_ptr app(testing::CreateApp()); + auto request = + VerifyAssertionRequest::FromIdToken("APIKEY", "provider", "id_token"); + CheckUrl(*request); +} + +TEST(VerifyAssertionTest, TestVerifyAssertionRequest_FromAccessToken) { + std::unique_ptr app(testing::CreateApp()); + auto request = VerifyAssertionRequest::FromAccessToken("APIKEY", "provider", + "access_token"); + CheckUrl(*request); +} + +TEST(VerifyAssertionTest, TestVerifyAssertionRequest_FromAccessTokenAndSecret) { + std::unique_ptr app(testing::CreateApp()); + auto request = VerifyAssertionRequest::FromAccessTokenAndOAuthSecret( + "APIKEY", "provider", "access_token", "oauth_secret"); + CheckUrl(*request); +} + +TEST(VerifyAssertionTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyAssertionResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"INVALID_IDP_RESPONSE\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorInvalidCredential, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ(0, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/verify_custom_token_test.cc b/auth/tests/desktop/rpcs/verify_custom_token_test.cc new file mode 100644 index 0000000000..3e13b552e1 --- /dev/null +++ b/auth/tests/desktop/rpcs/verify_custom_token_test.cc @@ -0,0 +1,90 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/verify_custom_token_request.h" +#include "auth/src/desktop/rpcs/verify_custom_token_response.h" +#include "auth/tests/desktop/rpcs/test_util.h" + +namespace firebase { +namespace auth { + +// Test VerifyCustomTokenRequest +TEST(VerifyCustomTokenTest, TestVerifyCustomTokenRequest) { + std::unique_ptr app(testing::CreateApp()); + VerifyCustomTokenRequest request("APIKEY", "token123"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyCustomToken?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " returnSecureToken: true,\n" + " token: \"token123\"\n" + "}\n", + request.options().post_fields); +} + +// Test VerifyCustomTokenResponse +TEST(VerifyCustomTokenTest, TestVerifyCustomTokenResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyCustomTokenResponse response; + // An example HTTP response JSON. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#VerifyCustomTokenResponse\",\n" + " \"idToken\": \"idtoken123\",\n" + " \"refreshToken\": \"refreshtoken123\",\n" + " \"expiresIn\": \"3600\",\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); +} + +TEST(VerifyCustomTokenTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyCustomTokenResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"CREDENTIAL_MISMATCH\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorCustomTokenMismatch, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ(0, response.expires_in()); +} +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/rpcs/verify_password_test.cc b/auth/tests/desktop/rpcs/verify_password_test.cc new file mode 100644 index 0000000000..04fecad20d --- /dev/null +++ b/auth/tests/desktop/rpcs/verify_password_test.cc @@ -0,0 +1,105 @@ +// Copyright 2017 Google LLC +// +// 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 "app/rest/transport_builder.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/rpcs/verify_password_request.h" +#include "auth/src/desktop/rpcs/verify_password_response.h" + +namespace firebase { +namespace auth { + +// Test VerifyPasswordRequest +TEST(VerifyPasswordTest, TestVerifyPasswordRequest) { + std::unique_ptr app(testing::CreateApp()); + VerifyPasswordRequest request("APIKEY", "abc@email", "pwd"); + EXPECT_EQ( + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=APIKEY", + request.options().url); + EXPECT_EQ( + "{\n" + " email: \"abc@email\",\n" + " password: \"pwd\",\n" + " returnSecureToken: true\n" + "}\n", + request.options().post_fields); +} + +// Test VerifyPasswordResponse +TEST(VerifyPasswordTest, TestVerifyPasswordResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyPasswordResponse response; + // An example HTTP response JSON in the exact format we get from real server + // with token string replaced by dummy string. + const char body[] = + "{\n" + " \"kind\": \"identitytoolkit#VerifyPasswordResponse\",\n" + " \"localId\": \"localid123\",\n" + " \"email\": \"abc@email\",\n" + " \"displayName\": \"ABC\",\n" + " \"idToken\": \"idtoken123\",\n" + " \"registered\": true,\n" + " \"refreshToken\": \"refreshtoken123\",\n" + " \"expiresIn\": \"3600\",\n" + " \"photoUrl\": \"dp.google\"\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + EXPECT_EQ("localid123", response.local_id()); + EXPECT_EQ("abc@email", response.email()); + EXPECT_EQ("ABC", response.display_name()); + EXPECT_EQ("idtoken123", response.id_token()); + EXPECT_EQ("refreshtoken123", response.refresh_token()); + EXPECT_EQ("dp.google", response.photo_url()); + EXPECT_EQ(3600, response.expires_in()); +} + +TEST(VerifyPasswordTest, TestErrorResponse) { + std::unique_ptr app(testing::CreateApp()); + VerifyPasswordResponse response; + const char body[] = + "{\n" + " \"error\": {\n" + " \"code\": 400,\n" + " \"message\": \"WEAK_PASSWORD\",\n" + " \"errors\": [\n" + " {\n" + " \"reason\": \"some reason\"\n" + " }\n" + " ]\n" + " }\n" + "}"; + response.ProcessBody(body, sizeof(body)); + response.MarkCompleted(); + + EXPECT_EQ(kAuthErrorWeakPassword, response.error_code()); + + // Make sure response doesn't crash on access. + EXPECT_EQ("", response.local_id()); + EXPECT_EQ("", response.email()); + EXPECT_EQ("", response.display_name()); + EXPECT_EQ("", response.id_token()); + EXPECT_EQ("", response.refresh_token()); + EXPECT_EQ("", response.photo_url()); + EXPECT_EQ(0, response.expires_in()); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/test_utils.cc b/auth/tests/desktop/test_utils.cc new file mode 100644 index 0000000000..de86beed5a --- /dev/null +++ b/auth/tests/desktop/test_utils.cc @@ -0,0 +1,71 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/tests/desktop/test_utils.h" + +namespace firebase { +namespace auth { +namespace test { + +namespace detail { +ListenerChangeCounter::ListenerChangeCounter() + : actual_changes_(0), expected_changes_(-1) {} + +ListenerChangeCounter::~ListenerChangeCounter() { Verify(); } + +void ListenerChangeCounter::ExpectChanges(const int num) { + expected_changes_ = num; +} +void ListenerChangeCounter::VerifyAndReset() { + Verify(); + expected_changes_ = -1; + actual_changes_ = 0; +} + +void ListenerChangeCounter::Verify() { + if (expected_changes_ != -1) { + EXPECT_EQ(expected_changes_, actual_changes_); + } +} + +} // namespace detail + +void IdTokenChangesCounter::OnIdTokenChanged(Auth* const /*unused*/) { + ++actual_changes_; +} + +void AuthStateChangesCounter::OnAuthStateChanged(Auth* const /*unused*/) { + ++actual_changes_; +} + +using ::testing::NotNull; +using ::testing::StrNe; + +void WaitForFuture(const firebase::Future& future, + const firebase::auth::AuthError expected_error) { + while (future.status() == firebase::kFutureStatusPending) { + } + [&] { + ASSERT_EQ(firebase::kFutureStatusComplete, future.status()); + EXPECT_EQ(expected_error, future.error()); + if (expected_error != kAuthErrorNone) { + EXPECT_THAT(future.error_message(), NotNull()); + EXPECT_THAT(future.error_message(), StrNe("")); + } + }(); +} + +} // namespace test +} // namespace auth +} // namespace firebase diff --git a/auth/tests/desktop/test_utils.h b/auth/tests/desktop/test_utils.h new file mode 100644 index 0000000000..2677abdbec --- /dev/null +++ b/auth/tests/desktop/test_utils.h @@ -0,0 +1,295 @@ +// Copyright 2017 Google LLC +// +// 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. + +#ifndef FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_TEST_UTILS_H_ +#define FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_TEST_UTILS_H_ + +#include "app/src/include/firebase/future.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/auth_desktop.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/types.h" + +namespace firebase { +namespace auth { +namespace test { + +namespace detail { +// Base class to test how many times a listener was called. +// Register one of the implementations below with the Auth class +// (IdToken/AuthStateChangesCounter), then call ExpectChanges(number) on it. By +// default, the check will be done in the destructor, but you can call +// VerifyAndReset to force the check while the test is still running, which is +// useful if the test involves several sign in operations. +class ListenerChangeCounter { + public: + ListenerChangeCounter(); + virtual ~ListenerChangeCounter(); + + void ExpectChanges(int num); + void VerifyAndReset(); + + protected: + int actual_changes_; + + private: + void Verify(); + + int expected_changes_; +}; +} // namespace detail + +inline FederatedAuthProvider::AuthenticatedUserData +GetFakeAuthenticatedUserData() { + FederatedAuthProvider::AuthenticatedUserData user_data; + user_data.uid = "localid123"; + user_data.email = "testsignin@example.com"; + user_data.display_name = ""; + user_data.photo_url = ""; + user_data.provider_id = "Firebase"; + user_data.is_email_verified = false; + user_data.raw_user_info["login"] = Variant("test_login@example.com"); + user_data.raw_user_info["screen_name"] = Variant("test_screen_name"); + user_data.access_token = "12345ABC"; + user_data.refresh_token = "67890DEF"; + user_data.token_expires_in_seconds = 60; + return user_data; +} + +inline void VerifySignInResult(const Future& future, + AuthError auth_error, + const char* error_message) { + EXPECT_EQ(future.status(), kFutureStatusComplete); + EXPECT_EQ(future.error(), auth_error); + if (error_message != nullptr) { + EXPECT_STREQ(future.error_message(), error_message); + } +} + +inline void VerifySignInResult(const Future& future, + AuthError auth_error) { + VerifySignInResult(future, auth_error, + /*error_message=*/nullptr); + EXPECT_EQ(future.error(), auth_error); +} + +inline FederatedOAuthProviderData GetFakeOAuthProviderData() { + FederatedOAuthProviderData provider_data; + provider_data.provider_id = + firebase::auth::GitHubAuthProvider::kProviderId; + provider_data.scopes = {"read:user", "user:email"}; + provider_data.custom_parameters = {{"req_id", "1234"}}; + return provider_data; +} + +// OAuthProviderHandler to orchestrate Auth::SignInWithProvider, +// User::LinkWithProvider and User::ReauthenticateWithProver tests. Provides +// a mechanism to test the callback surface of the FederatedAuthProvider. +// Additionally the class provides option checks (extra_integrity_checks) to +// ensure the validity of the data that the Auth implementation passes +// to the handler, such as a non-null auth completion handle. +class OAuthProviderTestHandler + : public FederatedAuthProvider::Handler { + public: + explicit OAuthProviderTestHandler(bool extra_integrity_checks = false) { + extra_integrity_checks_ = extra_integrity_checks; + authenticated_user_data_ = GetFakeAuthenticatedUserData(); + sign_in_auth_completion_handle_ = nullptr; + link_auth_completion_handle_ = nullptr; + reauthenticate_auth_completion_handle_ = nullptr; + } + + explicit OAuthProviderTestHandler( + const FederatedAuthProvider::AuthenticatedUserData& + authenticated_user_data, + bool extra_integrity_checks = false) { + extra_integrity_checks_ = extra_integrity_checks; + authenticated_user_data_ = authenticated_user_data; + sign_in_auth_completion_handle_ = nullptr; + } + + void SetAuthenticatedUserData( + const FederatedAuthProvider::AuthenticatedUserData& user_data) { + authenticated_user_data_ = user_data; + } + + FederatedAuthProvider::AuthenticatedUserData* GetAuthenticatedUserData() { + return &authenticated_user_data_; + } + + // Caches the auth_completion_handler, which will be invoked via + // the test framework's inovcation of the TriggerSignInComplete method. + void OnSignIn(const FederatedOAuthProviderData& provider_data, + AuthCompletionHandle* completion_handle) override { + // ensure we're not invoking this handler twice, thereby overwritting the + // sign_in_auth_completion_handle_ + assert(sign_in_auth_completion_handle_ == nullptr); + sign_in_auth_completion_handle_ = completion_handle; + PerformIntegrityChecks(provider_data, completion_handle); + } + + // Invokes SignInComplete with the auth completion handler provided to this + // during the Auth::SignInWithProvider flow. The ability to trigger this from + // the test framework, instead of immediately from OnSignIn, provides + // mechanisms to test multiple on-going authentication/sign-in requests on + // the Auth object. + void TriggerSignInComplete() { + assert(sign_in_auth_completion_handle_); + SignInComplete(sign_in_auth_completion_handle_, authenticated_user_data_, + /*auth_error=*/kAuthErrorNone, ""); + } + + // Invokes SignInComplete with specific auth error codes and error messages. + void TriggerSignInCompleteWithError(AuthError auth_error, + const char* error_message) { + assert(sign_in_auth_completion_handle_); + SignInComplete(sign_in_auth_completion_handle_, authenticated_user_data_, + auth_error, error_message); + } + + // Caches the auth_completion_handler, which will be invoked via + // the test framework's inovcation of the TriggerLinkComplete method. + void OnLink(const FederatedOAuthProviderData& provider_data, + AuthCompletionHandle* completion_handle) override { + assert(link_auth_completion_handle_ == nullptr); + link_auth_completion_handle_ = completion_handle; + PerformIntegrityChecks(provider_data, completion_handle); + } + + // Invokes LinkComplete with the auth completion handler provided to this + // during the User::LinkWithProvider flow. The ability to trigger this from + // the test framework, instead of immediately from OnLink, provides + // mechanisms to test multiple on-going authentication/link requests on + // the User object. + void TriggerLinkComplete() { + assert(link_auth_completion_handle_); + LinkComplete(link_auth_completion_handle_, authenticated_user_data_, + /*auth_error=*/kAuthErrorNone, ""); + } + + // Invokes Link Complete with a specific auth error code and error message + void TriggerLinkCompleteWithError(AuthError auth_error, + const char* error_message) { + assert(link_auth_completion_handle_); + LinkComplete(link_auth_completion_handle_, authenticated_user_data_, + auth_error, error_message); + } + + // Caches the auth_completion_handler, which will be invoked via + // the test framework's inovcation of the TriggerReauthenticateComplete + // method. + void OnReauthenticate(const FederatedOAuthProviderData& provider_data, + AuthCompletionHandle* completion_handle) override { + assert(reauthenticate_auth_completion_handle_ == nullptr); + reauthenticate_auth_completion_handle_ = completion_handle; + PerformIntegrityChecks(provider_data, completion_handle); + } + + // Invokes ReauthenticateComplete with the auth completion handler provided to + // this during the User::ReauthenticateWithProvider flow. The ability to + // trigger this from the test framework, instead of immediately from + // OnReauthneticate, provides mechanisms to test multiple on-going + // re-authentication requests on the User object. + void TriggerReauthenticateComplete() { + assert(reauthenticate_auth_completion_handle_); + ReauthenticateComplete(reauthenticate_auth_completion_handle_, + authenticated_user_data_, + /*auth_error=*/kAuthErrorNone, ""); + } + + // Invokes ReauthenticateComplete with a specific auth error code and error + // message + void TriggerReauthenticateCompleteWithError(AuthError auth_error, + const char* error_message) { + assert(reauthenticate_auth_completion_handle_); + ReauthenticateComplete(reauthenticate_auth_completion_handle_, + authenticated_user_data_, auth_error, error_message); + } + + private: + void PerformIntegrityChecks(const FederatedOAuthProviderData& provider_data, + const AuthCompletionHandle* completion_handle) { + if (extra_integrity_checks_) { + // check the auth_completion_handle the implementation provided. + // note that the auth completion handle is an opaque type for our users, + // and normal applications wouldn't get a chance to do these sorts of + // checks. + EXPECT_NE(completion_handle, nullptr); + + // ensure that the auth data object has been configured in the handle. + assert(completion_handle->auth_data); + EXPECT_EQ(completion_handle->auth_data->future_impl.GetFutureStatus( + completion_handle->future_handle.get()), + kFutureStatusPending); + FederatedOAuthProviderData expected_provider_data = + GetFakeOAuthProviderData(); + EXPECT_EQ(provider_data.provider_id, expected_provider_data.provider_id); + EXPECT_EQ(provider_data.scopes, expected_provider_data.scopes); + EXPECT_EQ(provider_data.custom_parameters, + expected_provider_data.custom_parameters); + } + } + + AuthCompletionHandle* sign_in_auth_completion_handle_; + AuthCompletionHandle* link_auth_completion_handle_; + AuthCompletionHandle* reauthenticate_auth_completion_handle_; + FederatedAuthProvider::AuthenticatedUserData authenticated_user_data_; + bool extra_integrity_checks_; +}; + +class IdTokenChangesCounter : public detail::ListenerChangeCounter, + public IdTokenListener { + public: + void OnIdTokenChanged(Auth* /*unused*/) override; +}; + +class AuthStateChangesCounter : public detail::ListenerChangeCounter, + public AuthStateListener { + public: + void OnAuthStateChanged(Auth* /*unused*/) override; +}; + +// Waits until the given future is complete and asserts that it completed with +// the given error (no error by default). Returns the future's result. +template +T WaitForFuture(const firebase::Future& future, + const firebase::auth::AuthError expected_error = + firebase::auth::kAuthErrorNone) { + while (future.status() == firebase::kFutureStatusPending) { + } + // This is wrapped in a lambda to work around the assertion macro expecting + // the function to return void. + [&] { + ASSERT_EQ(firebase::kFutureStatusComplete, future.status()); + EXPECT_EQ(expected_error, future.error()); + if (expected_error != kAuthErrorNone) { + EXPECT_THAT(future.error_message(), ::testing::NotNull()); + EXPECT_THAT(future.error_message(), ::testing::StrNe("")); + } + }(); + return *future.result(); +} + +// Waits until the given future is complete and asserts that it completed with +// the given error (no error by default). +void WaitForFuture( + const firebase::Future& future, + firebase::auth::AuthError expected_error = firebase::auth::kAuthErrorNone); + +} // namespace test +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_CLIENT_CPP_TESTS_DESKTOP_TEST_UTILS_H_ diff --git a/auth/tests/desktop/user_desktop_test.cc b/auth/tests/desktop/user_desktop_test.cc new file mode 100644 index 0000000000..a0bd7c1377 --- /dev/null +++ b/auth/tests/desktop/user_desktop_test.cc @@ -0,0 +1,1217 @@ +// Copyright 2017 Google LLC +// +// 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 "auth/src/desktop/user_desktop.h" + +#include "app/rest/transport_builder.h" +#include "app/rest/transport_curl.h" +#include "app/rest/transport_mock.h" +#include "app/src/include/firebase/app.h" +#include "app/src/mutex.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "auth/src/desktop/auth_desktop.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/user.h" +#include "auth/tests/desktop/fakes.h" +#include "auth/tests/desktop/test_utils.h" +#include "testing/config.h" +#include "testing/ticker.h" +#include "flatbuffers/stl_emulation.h" + +namespace firebase { +namespace auth { + +using test::CreateErrorHttpResponse; +using test::FakeSetT; +using test::FakeSuccessfulResponse; +using test::GetFakeOAuthProviderData; +using test::GetUrlForApi; +using test::InitializeConfigWithAFake; +using test::InitializeConfigWithFakes; +using test::OAuthProviderTestHandler; +using test::VerifySignInResult; +using test::WaitForFuture; + +using ::testing::AnyOf; +using ::testing::IsEmpty; + +namespace { + +const char* const API_KEY = "MY-FAKE-API-KEY"; +// Constant, describing how many times we would like to sleep 1ms to wait +// for loading persistence cache. +const int kWaitForLoadMaxTryout = 500; + +void InitializeSignUpFlowFakes() { + FakeSetT fakes; + + fakes[GetUrlForApi(API_KEY, "signupNewUser")] = + FakeSuccessfulResponse("SignupNewUserResponse", + " \"idToken\": \"idtoken123\"," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"," + " \"localId\": \"localid123\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + FakeSuccessfulResponse("GetAccountInfoResponse", + " \"users\": [" + " {" + " \"localId\": \"localid123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"456\"" + " }" + " ]"); + + InitializeConfigWithFakes(fakes); +} + +std::string GetSingleFakeProvider(const std::string& provider_id) { + // clang-format off + return std::string( + " {" + " \"federatedId\": \"fake_uid\"," + " \"email\": \"fake_email@example.com\"," + " \"displayName\": \"fake_display_name\"," + " \"photoUrl\": \"fake_photo_url\"," + " \"providerId\": \"") + provider_id + "\"," + " \"phoneNumber\": \"123123\"" + " }"; + // clang-format on +} + +std::string GetFakeProviderInfo( + const std::string& provider_id = "fake_provider_id") { + return std::string("\"providerUserInfo\": [") + + GetSingleFakeProvider(provider_id) + "]"; +} + +std::string FakeSetAccountInfoResponse() { + return FakeSuccessfulResponse( + "SetAccountInfoResponse", + std::string("\"localId\": \"fake_local_id\"," + "\"email\": \"new_fake_email@example.com\"," + "\"idToken\": \"new_fake_token\"," + "\"expiresIn\": \"3600\"," + "\"passwordHash\": \"new_fake_hash\"," + "\"emailVerified\": false,") + + GetFakeProviderInfo()); +} + +std::string FakeSetAccountInfoResponseWithDetails() { + return FakeSuccessfulResponse( + "SetAccountInfoResponse", + std::string("\"localId\": \"fake_local_id\"," + "\"email\": \"new_fake_email@example.com\"," + "\"idToken\": \"new_fake_token2\"," + "\"expiresIn\": \"3600\"," + "\"passwordHash\": \"new_fake_hash\"," + "\"displayName\": \"Fake Name\"," + "\"photoUrl\": \"https://fake_url.com\"," + "\"emailVerified\": false,") + + GetFakeProviderInfo()); +} + +std::string FakeVerifyAssertionResponse() { + return FakeSuccessfulResponse("VerifyAssertionResponse", + "\"isNewUser\": true," + "\"localId\": \"localid123\"," + "\"idToken\": \"verify_idtoken123\"," + "\"providerId\": \"google.com\"," + "\"refreshToken\": \"verify_refreshtoken123\"," + "\"expiresIn\": \"3600\""); +} + +std::string FakeGetAccountInfoResponse() { + return FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\",") + + GetFakeProviderInfo() + + " }" + " ]"); +} + +std::string CreateGetAccountInfoFake() { + return FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\",") + + GetFakeProviderInfo() + + " }" + " ]"); +} + +void InitializeAuthorizeWithProviderFakes( + const std::string& get_account_info_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = get_account_info_response; + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulAuthenticateWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + const std::string& get_account_info_response) { + InitializeAuthorizeWithProviderFakes(get_account_info_response); + provider->SetProviderData(GetFakeOAuthProviderData()); + provider->SetAuthHandler(handler); +} + +void InitializeSuccessfulAuthenticateWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler) { + InitializeSuccessfulAuthenticateWithProviderFlow(provider, handler, + CreateGetAccountInfoFake()); +} + +void VerifyUser(const User& user) { + EXPECT_EQ("localid123", user.uid()); + EXPECT_EQ("testsignin@example.com", user.email()); + EXPECT_EQ("", user.display_name()); + EXPECT_EQ("", user.photo_url()); + EXPECT_EQ("Firebase", user.provider_id()); + EXPECT_EQ("", user.phone_number()); + EXPECT_FALSE(user.is_email_verified()); +} + +void VerifyProviderData(const User& user) { + const std::vector& provider_data = user.provider_data(); + EXPECT_EQ(1, provider_data.size()); + if (provider_data.empty()) { + return; // Avoid crashing on vector out-of-bounds access below + } + EXPECT_EQ("fake_uid", provider_data[0]->uid()); + EXPECT_EQ("fake_email@example.com", provider_data[0]->email()); + EXPECT_EQ("fake_display_name", provider_data[0]->display_name()); + EXPECT_EQ("fake_photo_url", provider_data[0]->photo_url()); + EXPECT_EQ("fake_provider_id", provider_data[0]->provider_id()); + EXPECT_EQ("123123", provider_data[0]->phone_number()); +} + +void InitializeSuccessfulVerifyAssertionFlow( + const std::string& verify_assertion_response) { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = verify_assertion_response; + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = FakeGetAccountInfoResponse(); + InitializeConfigWithFakes(fakes); +} + +void InitializeSuccessfulVerifyAssertionFlow() { + InitializeSuccessfulVerifyAssertionFlow(FakeVerifyAssertionResponse()); +} + +bool WaitOnLoadPersistence(AuthData* auth_data) { + bool load_finished = false; + int load_wait_counter = 0; + while (!load_finished) { + if (load_wait_counter >= kWaitForLoadMaxTryout) { + break; + } + load_wait_counter++; + firebase::internal::Sleep(1); + { + MutexLock lock(auth_data->listeners_mutex); + load_finished = !auth_data->persistent_cache_load_pending; + } + } + return load_finished; +} + +} // namespace + +class UserDesktopTest : public ::testing::Test { + protected: + UserDesktopTest() : sem_(0) {} + + void SetUp() override { + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); + AppOptions options = testing::MockAppOptions(); + options.set_api_key(API_KEY); + firebase_app_ = std::unique_ptr(testing::CreateApp(options)); + firebase_auth_ = std::unique_ptr(Auth::GetAuth(firebase_app_.get())); + + InitializeSignUpFlowFakes(); + + firebase_auth_->AddIdTokenListener(&id_token_listener); + firebase_auth_->AddAuthStateListener(&auth_state_listener); + + WaitOnLoadPersistence(firebase_auth_->auth_data_); + + // Current user should be updated upon successful anonymous sign-in. + // Should expect one extra trigger during either listener add after load + // credential is done, or load finish after listener added, so changed + // twice. + id_token_listener.ExpectChanges(2); + auth_state_listener.ExpectChanges(2); + + Future future = firebase_auth_->SignInAnonymously(); + while (future.status() == kFutureStatusPending) { + } + firebase_user_ = firebase_auth_->current_user(); + EXPECT_NE(nullptr, firebase_user_); + + // Reset listeners before tests are run. + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + } + + void TearDown() override { + // Reset listeners before signing out. + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + firebase_auth_->SignOut(); + firebase_auth_.reset(nullptr); + firebase_app_.reset(nullptr); + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + Future ProcessLinkWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + bool trigger_link) { + InitializeSuccessfulAuthenticateWithProviderFlow(provider, handler); + Future future = firebase_user_->LinkWithProvider(provider); + if (trigger_link) { + handler->TriggerLinkComplete(); + } + return future; + } + + Future ProcessReauthenticateWithProviderFlow( + FederatedOAuthProvider* provider, OAuthProviderTestHandler* handler, + bool trigger_reauthenticate) { + InitializeSuccessfulAuthenticateWithProviderFlow(provider, handler); + Future future = + firebase_user_->ReauthenticateWithProvider(provider); + if (trigger_reauthenticate) { + handler->TriggerReauthenticateComplete(); + } + return future; + } + + std::unique_ptr firebase_app_; + std::unique_ptr firebase_auth_; + User* firebase_user_ = nullptr; + + test::IdTokenChangesCounter id_token_listener; + test::AuthStateChangesCounter auth_state_listener; + + Semaphore sem_; +}; + +// Test that metadata is correctly being populated and exposed +TEST_F(UserDesktopTest, TestAccountMetadata) { + EXPECT_EQ(123, + firebase_auth_->current_user()->metadata().last_sign_in_timestamp); + EXPECT_EQ(456, firebase_auth_->current_user()->metadata().creation_timestamp); +} + +TEST_F(UserDesktopTest, TestGetToken) { + const auto api_url = + std::string("https://securetoken.googleapis.com/v1/token?key=") + API_KEY; + InitializeConfigWithAFake( + api_url, + FakeSuccessfulResponse("\"access_token\": \"new accesstoken123\"," + "\"expires_in\": \"3600\"," + "\"token_type\": \"Bearer\"," + "\"refresh_token\": \"new refreshtoken123\"," + "\"id_token\": \"new idtoken123\"," + "\"user_id\": \"localid123\"," + "\"project_id\": \"53101460582\"")); + + // Token should change, but user stays the same. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + // Call the function and verify results. + std::string token = WaitForFuture(firebase_user_->GetToken(false)); + EXPECT_EQ("idtoken123", token); + + // Call again won't change token since it is still valid. + token = WaitForFuture(firebase_user_->GetToken(false)); + EXPECT_NE("new idtoken123", token); + + // Call again to force refreshing token. + const std::string new_token = WaitForFuture(firebase_user_->GetToken(true)); + EXPECT_NE(token, new_token); + EXPECT_EQ("new idtoken123", new_token); +} + +TEST_F(UserDesktopTest, TestDelete) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "deleteAccount"), + FakeSuccessfulResponse("DeleteAccountResponse", "")); + + // Expect logout. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + EXPECT_FALSE(firebase_user_->uid().empty()); + WaitForFuture(firebase_user_->Delete()); + EXPECT_TRUE(firebase_user_->uid().empty()); +} + +TEST_F(UserDesktopTest, TestSendEmailVerification) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "getOobConfirmationCode"), + FakeSuccessfulResponse("GetOobConfirmationCodeResponse", + "\"email\": \"fake_email@example.com\"")); + + // Sending email shouldn't affect the current user in any way. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->SendEmailVerification()); +} + +TEST_F(UserDesktopTest, TestReload) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "getAccountInfo"), + FakeSuccessfulResponse( + "GetAccountInfoResponse", + std::string("\"users\": [" + " {" + " \"localId\": \"fake_local_id\"," + " \"email\": \"fake_email@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"fake_hash\"," + " \"passwordUpdatedAt\": 1.509402565E12," + // Note: these values are copied from an actual + // backend response, so it seems that backend uses + // seconds for validSince but microseconds for the + // other time fields. + " \"validSince\": \"1509402565\"," + " \"lastLoginAt\": \"1509402565000\"," + " \"createdAt\": \"1509402565000\",") + + GetFakeProviderInfo() + + " }" + "]")); + + // User stayed the same, and GetAccountInfoResponse doesn't contain tokens. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->Reload()); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of setting a new email on the currently logged in user. +TEST_F(UserDesktopTest, TestUpdateEmail) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const std::string new_email = "new_fake_email@example.com"; + + EXPECT_NE(new_email, firebase_user_->email()); + WaitForFuture(firebase_user_->UpdateEmail(new_email.c_str())); + EXPECT_EQ(new_email, firebase_user_->email()); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of setting a new password on the currently logged in +// user. +TEST_F(UserDesktopTest, TestUpdatePassword) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->UpdatePassword("new_password")); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of setting new profile properties (display name and +// photo URL) on the currently logged in user. +TEST_F(UserDesktopTest, TestUpdateProfile_Update) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponseWithDetails()); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const std::string display_name = "Fake Name"; + const std::string photo_url = "https://fake_url.com"; + User::UserProfile profile; + profile.display_name = display_name.c_str(); + profile.photo_url = photo_url.c_str(); + + EXPECT_NE(display_name, firebase_user_->display_name()); + EXPECT_NE(photo_url, firebase_user_->photo_url()); + WaitForFuture(firebase_user_->UpdateUserProfile(profile)); + EXPECT_EQ(display_name, firebase_user_->display_name()); + EXPECT_EQ(photo_url, firebase_user_->photo_url()); + VerifyProviderData(*firebase_user_); +} + +// Tests the happy case of deleting profile properties from the currently logged +// in user (setting display name and photo URL to be blank). +TEST_F(UserDesktopTest, TestUpdateProfile_Delete) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponseWithDetails()); + + const std::string display_name = "Fake Name"; + const std::string photo_url = "https://fake_url.com"; + User::UserProfile profile; + profile.display_name = display_name.c_str(); + profile.photo_url = photo_url.c_str(); + + WaitForFuture(firebase_user_->UpdateUserProfile(profile)); + EXPECT_EQ(display_name, firebase_user_->display_name()); + EXPECT_EQ(photo_url, firebase_user_->photo_url()); + + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + User::UserProfile blank_profile; + blank_profile.display_name = blank_profile.photo_url = ""; + WaitForFuture(firebase_user_->UpdateUserProfile(blank_profile)); + EXPECT_TRUE(firebase_user_->display_name().empty()); + EXPECT_TRUE(firebase_user_->photo_url().empty()); +} + +// Tests the happy case of unlinking a provider from the currently logged in +// user. +TEST_F(UserDesktopTest, TestUnlink) { + FakeSetT fakes; + // So that the user has an associated provider + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = FakeGetAccountInfoResponse(); + fakes[GetUrlForApi(API_KEY, "setAccountInfo")] = FakeSetAccountInfoResponse(); + InitializeConfigWithFakes(fakes); + + // SetAccountInfoResponse contains a new token. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->Reload()); + WaitForFuture(firebase_user_->Unlink("fake_provider_id")); + VerifyProviderData(*firebase_user_); +} + +TEST_F(UserDesktopTest, TestUnlink_NonLinkedProvider) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + WaitForFuture(firebase_user_->Unlink("no_such_provider"), + kAuthErrorNoSuchProvider); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_OauthCredential) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Response contains a new ID token, but user should have stayed the same. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + EXPECT_TRUE(firebase_user_->is_anonymous()); + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const User* const user = + WaitForFuture(firebase_user_->LinkWithCredential(credential)); + EXPECT_FALSE(user->is_anonymous()); + VerifyUser(*user); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_EmailCredential) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // Response contains a new ID token, but user should have stayed the same. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const std::string new_email = "new_fake_email@example.com"; + + EXPECT_NE(new_email, firebase_user_->email()); + + EXPECT_TRUE(firebase_user_->is_anonymous()); + const Credential credential = + EmailAuthProvider::GetCredential(new_email.c_str(), "fake_password"); + WaitForFuture(firebase_user_->LinkWithCredential(credential)); + EXPECT_EQ(new_email, firebase_user_->email()); + EXPECT_FALSE(firebase_user_->is_anonymous()); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_NeedsConfirmation) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "verifyAssertion"), + FakeSuccessfulResponse("verifyAssertion", "\"needConfirmation\": true")); + + // If response contains needConfirmation, the whole operation should fail, and + // current user should be unaffected. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->LinkWithCredential(credential), + kAuthErrorAccountExistsWithDifferentCredentials); +} + +TEST_F(UserDesktopTest, TestLinkWithCredential_ChecksAlreadyLinkedProviders) { + { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = + FakeVerifyAssertionResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = FakeSuccessfulResponse( + // clang-format off + "GetAccountInfoResponse", + std::string( + "\"users\":" + " [" + " {" + " \"localId\": \"localid123\",") + + GetFakeProviderInfo("google.com") + + " }" + " ]"); + // clang-format on + InitializeConfigWithFakes(fakes); + } + + // Upon linking, user should stay the same, but ID token should be updated. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential google_credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->LinkWithCredential(google_credential)); + + // The same provider shouldn't be linked twice. + WaitForFuture(firebase_user_->LinkWithCredential(google_credential), + kAuthErrorProviderAlreadyLinked); + + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + // Linking already linked provider, should fail, so current user shouldn't be + // updated at all. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + { + FakeSetT fakes; + fakes[GetUrlForApi(API_KEY, "verifyAssertion")] = + FakeVerifyAssertionResponse(); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + // clang-format off + FakeSuccessfulResponse("GetAccountInfoResponse", + std::string( + "\"users\":" + " [" + " {" + " \"localId\": \"localid123\"," + " \"providerUserInfo\": [") + + GetSingleFakeProvider("google.com") + "," + + GetSingleFakeProvider("facebook.com") + + " ]" + " }" + " ]"); + // clang-format on + InitializeConfigWithFakes(fakes); + } + + // Should be able to link a different provider. + const Credential facebook_credential = + FacebookAuthProvider::GetCredential("fake_access_token"); + WaitForFuture(firebase_user_->LinkWithCredential(facebook_credential)); + + // The same provider shouldn't be linked twice. + WaitForFuture(firebase_user_->LinkWithCredential(facebook_credential), + kAuthErrorProviderAlreadyLinked); + // Check that the previously linked provider wasn't overridden. + WaitForFuture(firebase_user_->LinkWithCredential(google_credential), + kAuthErrorProviderAlreadyLinked); +} + +TEST_F(UserDesktopTest, TestLinkWithCredentialAndRetrieveData) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Upon linking, user should stay the same, but ID token should be updated. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const SignInResult sign_in_result = WaitForFuture( + firebase_user_->LinkAndRetrieveDataWithCredential(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, TestReauthenticate) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Upon reauthentication, user should have stayed the same, but ID token + // should have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->Reauthenticate(credential)); +} + +TEST_F(UserDesktopTest, TestReauthenticate_NeedsConfirmation) { + InitializeConfigWithAFake( + GetUrlForApi(API_KEY, "verifyAssertion"), + FakeSuccessfulResponse("verifyAssertion", "\"needConfirmation\": true")); + + // If response contains needConfirmation, the whole operation should fail, and + // current user should be unaffected. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + WaitForFuture(firebase_user_->Reauthenticate(credential), + kAuthErrorAccountExistsWithDifferentCredentials); +} + +TEST_F(UserDesktopTest, TestReauthenticateAndRetrieveData) { + InitializeSuccessfulVerifyAssertionFlow(); + + // Upon reauthentication, user should have stayed the same, but ID token + // should have changed. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(0); + + const Credential credential = + GoogleAuthProvider::GetCredential("fake_id_token", ""); + const SignInResult sign_in_result = + WaitForFuture(firebase_user_->ReauthenticateAndRetrieveData(credential)); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + VerifyUser(*sign_in_result.user); +} + +// Checks that current user is signed out upon receiving errors from the +// backend indicating the user is no longer valid. +class UserDesktopTestSignOutOnError : public UserDesktopTest { + protected: + // Reduces boilerplate in similar tests checking for sign out in several API + // methods. + template + void CheckSignOutIfUserIsInvalid(const std::string& api_endpoint, + const std::string& backend_error, + const AuthError sdk_error, + const OperationT operation) { + // Receiving error from the backend should make the operation fail, and + // current user shouldn't be affected. + id_token_listener.ExpectChanges(0); + auth_state_listener.ExpectChanges(0); + + // First check that sign out doesn't happen on just any error + // (kAuthErrorOperationNotAllowed is chosen arbitrarily). + InitializeConfigWithAFake(api_endpoint, + CreateErrorHttpResponse("OPERATION_NOT_ALLOWED")); + EXPECT_FALSE(firebase_user_->uid().empty()); + WaitForFuture(operation(), kAuthErrorOperationNotAllowed); + EXPECT_FALSE(firebase_user_->uid().empty()); // User is still signed in. + + id_token_listener.VerifyAndReset(); + auth_state_listener.VerifyAndReset(); + // Expect sign out. + id_token_listener.ExpectChanges(1); + auth_state_listener.ExpectChanges(1); + + // Now check that the user will be logged out upon receiving a certain + // error from the backend. + InitializeConfigWithAFake(api_endpoint, + CreateErrorHttpResponse(backend_error)); + WaitForFuture(operation(), sdk_error); + EXPECT_THAT(firebase_user_->uid(), IsEmpty()); + } +}; + +TEST_F(UserDesktopTestSignOutOnError, Reauth) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "verifyAssertion"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->Reauthenticate( + GoogleAuthProvider::GetCredential("fake_id_token", "")); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, Reload) { + CheckSignOutIfUserIsInvalid(GetUrlForApi(API_KEY, "getAccountInfo"), + "USER_NOT_FOUND", kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->Reload(); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, UpdateEmail) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->UpdateEmail("fake_email@example.com"); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, UpdatePassword) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_DISABLED", + kAuthErrorUserDisabled, [&] { + sem_.Post(); + return firebase_user_->UpdatePassword("fake_password"); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, UpdateProfile) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "TOKEN_EXPIRED", + kAuthErrorUserTokenExpired, [&] { + sem_.Post(); + return firebase_user_->UpdateUserProfile(User::UserProfile()); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, Unlink) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "getAccountInfo"), + FakeGetAccountInfoResponse()); + WaitForFuture(firebase_user_->Reload()); + + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->Unlink("fake_provider_id"); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, LinkWithEmail) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->LinkWithCredential( + EmailAuthProvider::GetCredential("fake_email@example.com", + "fake_password")); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, LinkWithOauthCredential) { + CheckSignOutIfUserIsInvalid( + GetUrlForApi(API_KEY, "verifyAssertion"), "USER_NOT_FOUND", + kAuthErrorUserNotFound, [&] { + sem_.Post(); + return firebase_user_->LinkWithCredential( + GoogleAuthProvider::GetCredential("fake_id_token", "")); + }); + sem_.Wait(); +} + +TEST_F(UserDesktopTestSignOutOnError, GetToken) { + const auto api_url = + std::string("https://securetoken.googleapis.com/v1/token?key=") + API_KEY; + CheckSignOutIfUserIsInvalid(api_url, "USER_NOT_FOUND", kAuthErrorUserNotFound, + [&] { + sem_.Post(); + return firebase_user_->GetToken(true); + }); + sem_.Wait(); +} + +// This test is to expose potential race condition and is primarily intended to +// be run with --config=tsan +TEST_F(UserDesktopTest, TestRaceCondition_SetAccountInfoAndSignOut) { + InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), + FakeSetAccountInfoResponse()); + + // SignOut is engaged on the main thread, whereas UpdateEmail will be executed + // on the background thread; consequently, the order in which they are + // executed is not defined. Nevertheless, this should not lead to any data + // corruption, when UpdateEmail writes to user profile while it's being + // deleted by SignOut. Whichever method succeeds first, user must be signed + // out once both are finished: if SignOut finishes last, it overrides the + // updated user, and if UpdateEmail finishes last, it should note that there + // is no currently signed in user and fail with kAuthErrorUserNotFound. + + auto future = firebase_user_->UpdateEmail("some_email"); + firebase_auth_->SignOut(); + while (future.status() == firebase::kFutureStatusPending) { + } + + EXPECT_THAT(future.error(), AnyOf(kAuthErrorNone, kAuthErrorNoSignedInUser)); + EXPECT_EQ(nullptr, firebase_auth_->current_user()); +} + +// LinkWithProvider tests. +TEST_F(UserDesktopTest, TestLinkWithProviderReturnsUnsupportedError) { + FederatedOAuthProvider provider; + Future future = firebase_user_->LinkWithProvider(&provider); + EXPECT_EQ(future.result()->user, nullptr); + EXPECT_EQ(future.error(), kAuthErrorUnimplemented); + EXPECT_EQ(std::string(future.error_message()), + "Operation is not supported on non-mobile systems."); +} + +// TODO(drsanta) The following tests are disabled as the AuthHandler support has +// not yet been released. +TEST_F(UserDesktopTest, + DISABLED_TestLinkWithProviderAndHandlerPassingIntegrityChecks) { + FederatedOAuthProvider provider; + test::OAuthProviderTestHandler handler(/*extra_integrity_checks_=*/true); + InitializeSuccessfulAuthenticateWithProviderFlow(&provider, &handler); + + Future future = firebase_user_->LinkWithProvider(&provider); + handler.TriggerLinkComplete(); + SignInResult sign_in_result = WaitForFuture(future); +} + +TEST_F(UserDesktopTest, + DISABLED_TestPendingLinkWithProviderSecondConcurrentSignInFails) { + FederatedOAuthProvider provider1; + OAuthProviderTestHandler handler1; + InitializeSuccessfulAuthenticateWithProviderFlow(&provider1, &handler1); + + FederatedOAuthProvider provider2; + provider2.SetProviderData(GetFakeOAuthProviderData()); + + OAuthProviderTestHandler handler2; + provider2.SetAuthHandler(&handler2); + Future future1 = firebase_user_->LinkWithProvider(&provider1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = firebase_user_->LinkWithProvider(&provider2); + VerifySignInResult(future2, kAuthErrorFederatedProviderAreadyInUse); + handler1.TriggerLinkComplete(); + const SignInResult sign_in_result = WaitForFuture(future1); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkWithProviderSignInResultUserPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + FederatedAuthProvider::AuthenticatedUserData user_data = + *(handler.GetAuthenticatedUserData()); + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + EXPECT_NE(sign_in_result.user, nullptr); + EXPECT_EQ(sign_in_result.user->is_email_verified(), + user_data.is_email_verified); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + EXPECT_EQ(sign_in_result.user->uid(), user_data.uid); + EXPECT_EQ(sign_in_result.user->email(), user_data.email); + EXPECT_EQ(sign_in_result.user->display_name(), user_data.display_name); + EXPECT_EQ(sign_in_result.user->photo_url(), user_data.photo_url); + EXPECT_EQ(sign_in_result.user->provider_id(), user_data.provider_id); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullUIDFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->uid = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullDisplayNamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->display_name = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullUsernamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->user_name = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullPhotoUrlPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->photo_url = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullProvderIdFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->provider_id = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestLinkCompleteNuDISABLED_llAccessTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->access_token = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullRefreshTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->refresh_token = nullptr; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteExpiresInMaxUInt64Passes) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->token_expires_in_seconds = ULONG_MAX; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteErrorMessagePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/false); + const char* error_message = "oh nos!"; + handler.TriggerLinkCompleteWithError(kAuthErrorApiNotAvailable, + error_message); + VerifySignInResult(future, kAuthErrorApiNotAvailable, error_message); +} + +TEST_F(UserDesktopTest, DISABLED_TestLinkCompleteNullErrorMessageFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = + ProcessLinkWithProviderFlow(&provider, &handler, /*trigger_link=*/false); + handler.TriggerLinkCompleteWithError(kAuthErrorApiNotAvailable, nullptr); + VerifySignInResult(future, kAuthErrorApiNotAvailable); +} + +// ReauthenticateWithProvider tests. +TEST_F(UserDesktopTest, TestReauthentciateWithProviderReturnsUnsupportedError) { + FederatedOAuthProvider provider; + Future future = + firebase_user_->ReauthenticateWithProvider(&provider); + EXPECT_EQ(future.result()->user, nullptr); + EXPECT_EQ(future.error(), kAuthErrorUnimplemented); + EXPECT_EQ(std::string(future.error_message()), + "Operation is not supported on non-mobile systems."); +} + +// TODO(drsanta) The following tests are disabled as the AuthHandler support has +// not yet been released. +TEST_F( + UserDesktopTest, + DISABLED_TestReauthenticateWithProviderAndHandlerPassingIntegrityChecks) { + FederatedOAuthProvider provider; + test::OAuthProviderTestHandler handler(/*extra_integrity_checks_=*/true); + InitializeSuccessfulAuthenticateWithProviderFlow(&provider, &handler); + + Future future = + firebase_user_->ReauthenticateWithProvider(&provider); + handler.TriggerReauthenticateComplete(); + SignInResult sign_in_result = WaitForFuture(future); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateWithProviderSecondConcurrentSignInFails) { + FederatedOAuthProvider provider1; + OAuthProviderTestHandler handler1; + InitializeSuccessfulAuthenticateWithProviderFlow(&provider1, &handler1); + + FederatedOAuthProvider provider2; + provider2.SetProviderData(GetFakeOAuthProviderData()); + + OAuthProviderTestHandler handler2; + provider2.SetAuthHandler(&handler2); + Future future1 = + firebase_user_->ReauthenticateWithProvider(&provider1); + EXPECT_EQ(future1.status(), kFutureStatusPending); + Future future2 = + firebase_user_->ReauthenticateWithProvider(&provider2); + VerifySignInResult(future2, kAuthErrorFederatedProviderAreadyInUse); + handler1.TriggerReauthenticateComplete(); + const SignInResult sign_in_result = WaitForFuture(future1); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateWithProviderSignInResultUserPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + FederatedAuthProvider::AuthenticatedUserData user_data = + *(handler.GetAuthenticatedUserData()); + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + EXPECT_NE(sign_in_result.user, nullptr); + EXPECT_EQ(sign_in_result.user->is_email_verified(), + user_data.is_email_verified); + EXPECT_FALSE(sign_in_result.user->is_anonymous()); + EXPECT_EQ(sign_in_result.user->uid(), user_data.uid); + EXPECT_EQ(sign_in_result.user->email(), user_data.email); + EXPECT_EQ(sign_in_result.user->display_name(), user_data.display_name); + EXPECT_EQ(sign_in_result.user->photo_url(), user_data.photo_url); + EXPECT_EQ(sign_in_result.user->provider_id(), user_data.provider_id); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullUIDFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->uid = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullDisplayNamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->display_name = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullUsernamePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->user_name = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullPhotoUrlPasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->photo_url = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteNullProvderIdFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->provider_id = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullAccessTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->access_token = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullRefreshTokenFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->refresh_token = nullptr; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + VerifySignInResult(future, kAuthErrorInvalidAuthenticatedUserData); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteExpiresInMaxUInt64Passes) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + handler.GetAuthenticatedUserData()->token_expires_in_seconds = ULONG_MAX; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/true); + SignInResult sign_in_result = WaitForFuture(future); + VerifyProviderData(*sign_in_result.user); +} + +TEST_F(UserDesktopTest, DISABLED_TestReauthenticateCompleteErrorMessagePasses) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/false); + const char* error_message = "oh nos!"; + handler.TriggerReauthenticateCompleteWithError(kAuthErrorApiNotAvailable, + error_message); + VerifySignInResult(future, kAuthErrorApiNotAvailable, error_message); +} + +TEST_F(UserDesktopTest, + DISABLED_TestReauthenticateCompleteNullErrorMessageFails) { + FederatedOAuthProvider provider; + OAuthProviderTestHandler handler; + Future future = ProcessReauthenticateWithProviderFlow( + &provider, &handler, /*trigger_reauthenticate=*/false); + handler.TriggerReauthenticateCompleteWithError(kAuthErrorApiNotAvailable, + nullptr); + VerifySignInResult(future, kAuthErrorApiNotAvailable); +} + +} // namespace auth +} // namespace firebase diff --git a/auth/tests/user_test.cc b/auth/tests/user_test.cc new file mode 100644 index 0000000000..e38804c7cf --- /dev/null +++ b/auth/tests/user_test.cc @@ -0,0 +1,520 @@ +/* + * Copyright 2017 Google LLC + * + * 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. + */ + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "auth/src/include/firebase/auth.h" +#include "auth/src/include/firebase/auth/user.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/ticker.h" + +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) +#include "app/rest/transport_builder.h" +#include "app/rest/transport_mock.h" +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +namespace firebase { +namespace auth { + +namespace { + +// Wait for the Future completed when necessary. We do not do so for Android nor +// iOS since their test is based on Ticker-based fake. We do not do so for +// desktop stub since its Future completes immediately. +template +inline void MaybeWaitForFuture(const Future& future) { +// Desktop developer sdk has a small delay due to async calls. +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + // Once REST implementation is in, we should be able to check this. Almost + // always the return of last-result is ahead of the future completion. But + // right now, the return of last-result actually happens after future is + // completed. + // EXPECT_EQ(firebase::kFutureStatusPending, future.status()); + while (firebase::kFutureStatusPending == future.status()) { + } +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) +} + +const char* const SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE = + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "setAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"email\": \"new@email.com\"" + " }']" + " }" + " }"; + +const char* const VERIFY_PASSWORD_SUCCESSFUL_RESPONSE = + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "verifyPassword?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"idToken\": \"idtoken123\"," + " \"registered\": true," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"" + " }']" + " }" + " }"; + +const char* const GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE = + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " users: [{" + " \"localId\": \"localid123\"," + " \"email\": \"testsignin@example.com\"," + " \"emailVerified\": false," + " \"passwordHash\": \"abcdefg\"," + " \"passwordUpdatedAt\": 31415926," + " \"validSince\": \"123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"123\"," + " \"providerUserInfo\": [" + " {" + " \"providerId\": \"provider\"," + " }" + " ]" + " }]" + " }']" + " }" + " }"; + +} // anonymous namespace + +class UserTest : public ::testing::Test { + protected: + void SetUp() override { +#if defined(FIREBASE_WAIT_ASYNC_IN_TEST) + rest::SetTransportBuilder([]() -> flatbuffers::unique_ptr { + return flatbuffers::unique_ptr( + new rest::TransportMock()); + }); +#endif // defined(FIREBASE_WAIT_ASYNC_IN_TEST) + + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseAuth.signInAnonymously'," + " futuregeneric:{ticker:0}}," + " {fake:'FIRAuth.signInAnonymouslyWithCompletion:'," + " futuregeneric:{ticker:0}}," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "signupNewUser?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#SignupNewUserResponse\"," + " \"idToken\": \"idtoken123\"," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"," + " \"localId\": \"localid123\"" + "}',]" + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"users\": [{" + " \"localId\": \"localid123\"" + " }]}'," + " ]" + " }" + " }" + " ]" + "}"); + firebase_app_ = testing::CreateApp(); + firebase_auth_ = Auth::GetAuth(firebase_app_); + Future result = firebase_auth_->SignInAnonymously(); + MaybeWaitForFuture(result); + firebase_user_ = firebase_auth_->current_user(); + EXPECT_NE(nullptr, firebase_user_); + } + + void TearDown() override { + // We do not own firebase_user_ object. So just assign it to nullptr here. + firebase_user_ = nullptr; + delete firebase_auth_; + firebase_auth_ = nullptr; + delete firebase_app_; + firebase_app_ = nullptr; + // cppsdk needs to be the last thing torn down, because the mocks are still + // needed for parts of the firebase destructors. + firebase::testing::cppsdk::ConfigReset(); + } + + // A helper function to verify future result naively: (1) it completed after + // one ticker and (2) the result has no error. Since most of the function in + // user delegate the actual logic into the native SDK, this verification is + // enough for most of the test case unless we implement some logic into the + // fake, which is not necessary for unit test. + template + static void Verify(const Future result) { +// Fake Android & iOS implemented the delay. Desktop stub completed immediately. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + MaybeWaitForFuture(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(0, result.error()); + } + + App* firebase_app_ = nullptr; + Auth* firebase_auth_ = nullptr; + User* firebase_user_ = nullptr; +}; + +TEST_F(UserTest, TestGetToken) { + // Test get sign-in token. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.getIdToken', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.getIDTokenForcingRefresh:completion:'," + " futuregeneric:{ticker:1}}," + " {" + " fake: '" + "https://securetoken.googleapis.com/v1/token?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"access_token\": \"fake_access_token\"," + " \"expires_in\": \"3600\"," + " \"token_type\": \"Bearer\"," + " \"refresh_token\": \"fake_refresh_token\"," + " \"id_token\": \"fake_id_token\"," + " \"user_id\": \"fake_user_id\"," + " \"project_id\": \"fake_project_id\"" + " }']" + " }" + " }" + " ]" + "}"); + Future token = + firebase_user_->GetToken(false /* force_refresh, doesn't matter here */); + + Verify(token); + EXPECT_FALSE(token.result()->empty()); +} + +TEST_F(UserTest, TestGetProviderData) { + // Test get provider data. Right now, most of the sign-in does not have extra + // data coming from providers. + const std::vector& provider = + firebase_user_->provider_data(); + EXPECT_TRUE(provider.empty()); +} + +TEST_F(UserTest, TestUpdateEmail) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.updateEmail', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.updateEmail:completion:', futuregeneric:" + "{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + EXPECT_NE("new@email.com", firebase_user_->email()); + Future result = firebase_user_->UpdateEmail("new@email.com"); + +// Fake Android & iOS implemented the delay. Desktop stub completed immediately. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + EXPECT_NE("new@email.com", firebase_user_->email()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + MaybeWaitForFuture(result); + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(0, result.error()); + EXPECT_EQ("new@email.com", firebase_user_->email()); +} + +TEST_F(UserTest, TestUpdatePassword) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.updatePassword', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.updatePassword:completion:'," + " futuregeneric:{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->UpdatePassword("1234567"); + Verify(result); +} + +TEST_F(UserTest, TestUpdateUserProfile) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.updateProfile', futuregeneric:{ticker:1}}," + " {fake:'FIRUserProfileChangeRequest." + "commitChangesWithCompletion:'," + " futuregeneric:{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + User::UserProfile profile; + Future result = firebase_user_->UpdateUserProfile(profile); + Verify(result); +} + +TEST_F(UserTest, TestReauthenticate) { + // Test reauthenticate. + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.reauthenticate', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.reauthenticateWithCredential:completion:'," + " futuregeneric:{ticker:1}},") + + VERIFY_PASSWORD_SUCCESSFUL_RESPONSE + "," + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->Reauthenticate( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} + +#if !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) +TEST_F(UserTest, TestReauthenticateAndRetrieveData) { + // Test reauthenticate and retrieve data. + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.reauthenticateAndRetrieveData'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRUser.reauthenticateAndRetrieveDataWithCredential:" + "completion:'," + " futuregeneric:{ticker:1}},") + + VERIFY_PASSWORD_SUCCESSFUL_RESPONSE + "," + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->ReauthenticateAndRetrieveData( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} +#endif // !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +TEST_F(UserTest, TestSendEmailVerification) { + // Test send email verification. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.sendEmailVerification'," + " futuregeneric:{ticker:1}}," + " {fake:'FIRUser.sendEmailVerificationWithCompletion:'," + " futuregeneric:{ticker:1}}," + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getOobConfirmationCode?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#GetOobConfirmationCodeResponse\"," + " \"email\": \"fake_email@fake_domain.com\"" + " }']" + " }" + " }" + " ]" + "}"); + Future result = firebase_user_->SendEmailVerification(); + Verify(result); +} + +TEST_F(UserTest, TestLinkWithCredential) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.linkWithCredential', " + "futuregeneric:{ticker:1}}," + " {fake:'FIRUser.linkWithCredential:completion:'," + " futuregeneric:{ticker:1}},") + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->LinkWithCredential( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} + +#if !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) +TEST_F(UserTest, TestLinkAndRetrieveDataWithCredential) { + // Test link and retrieve data with credential. This calls the same native SDK + // function as LinkWithCredential(). + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.linkWithCredential', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.linkAndRetrieveDataWithCredential:completion:'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Future result = + firebase_user_->LinkAndRetrieveDataWithCredential( + EmailAuthProvider::GetCredential("i@email.com", "pw")); + Verify(result); +} +#endif // !defined(__APPLE__) && !defined(FIREBASE_WAIT_ASYNC_IN_TEST) + +TEST_F(UserTest, TestUnlink) { + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.unlink', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.unlinkFromProvider:completion:'," + " futuregeneric:{ticker:1}},") + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + "," + + SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + // Mobile wrappers and desktop have different implementations: desktop checks + // for valid provider before doing the RPC call, while wrappers leave that to + // platform implementation, which is faked out in the test. To minimize the + // divergence, for desktop only, first prepare server GetAccountInfo response + // which contains a provider, and then Reload, to make sure that the given + // provider ID is valid. For mobile wrappers, this will be a no-op. Use + // MaybeWaitForFuture because to Reload will return immediately for mobile + // wrappers, and Verify expects at least a single "tick". + MaybeWaitForFuture(firebase_user_->Reload()); + Future result = firebase_user_->Unlink("provider"); + Verify(result); + // For desktop, the provider must have been removed. For mobile wrappers, the + // whole flow must have been a no-op, and the provider list was empty to begin + // with. + EXPECT_TRUE(firebase_user_->provider_data().empty()); +} + +TEST_F(UserTest, TestReload) { + // Test reload. + const std::string config = + std::string( + "{" + " config:[" + " {fake:'FirebaseUser.reload', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.reloadWithCompletion:', " + "futuregeneric:{ticker:1}},") + + GET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + " ]" + "}"; + firebase::testing::cppsdk::ConfigSet(config.c_str()); + + Future result = firebase_user_->Reload(); + Verify(result); +} + +TEST_F(UserTest, TestDelete) { + // Test delete. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.delete', futuregeneric:{ticker:1}}," + " {fake:'FIRUser.deleteWithCompletion:', futuregeneric:{ticker:1}}," + " {" + " fake: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "deleteAccount?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#DeleteAccountResponse\"" + " }']" + " }" + " }" + " ]" + "}"); + Future result = firebase_user_->Delete(); + Verify(result); +} + +TEST_F(UserTest, TestIsEmailVerified) { + // Test is email verified. Right now both stub and fake will return false + // unanimously. + EXPECT_FALSE(firebase_user_->is_email_verified()); +} + +TEST_F(UserTest, TestIsAnonymous) { + // Test is anonymous. + EXPECT_TRUE(firebase_user_->is_anonymous()); +} + +TEST_F(UserTest, TestGetter) { +// Test getter functions. The fake value are different between stub and fake. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_EQ("fake email", firebase_user_->email()); + EXPECT_EQ("fake display name", firebase_user_->display_name()); + EXPECT_EQ("fake provider id", firebase_user_->provider_id()); +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + EXPECT_TRUE(firebase_user_->email().empty()); + EXPECT_TRUE(firebase_user_->display_name().empty()); + EXPECT_EQ("Firebase", firebase_user_->provider_id()); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || FIREBASE_PLATFORM_IOS + + EXPECT_FALSE(firebase_user_->uid().empty()); + EXPECT_TRUE(firebase_user_->photo_url().empty()); +} +} // namespace auth +} // namespace firebase diff --git a/binary_to_array_test.py b/binary_to_array_test.py new file mode 100644 index 0000000000..3deee34cb7 --- /dev/null +++ b/binary_to_array_test.py @@ -0,0 +1,91 @@ +# Copyright 2018 Google LLC +# +# 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. + +"""Tests for google3.firebase.app.client.cpp.binary_to_array.""" + +from google3.testing.pybase import googletest +from google3.firebase.app.client.cpp import binary_to_array + +EXPECTED_SOURCE_FILE = """\ +// Copyright 2016 Google Inc. All Rights Reserved. + +#include + +namespace test_outer_namespace { +namespace test_inner_namespace { + +extern const size_t test_array_name_size; +extern const char test_fileid[]; +extern const unsigned char test_array_name[]; + +const unsigned char test_array_name[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x00 // Extra \\0 to make it a C string +}; + +const size_t test_array_name_size = + sizeof(test_array_name) - 1; + +const char test_fileid[] = + "test_filename"; + +} // namespace test_inner_namespace +} // namespace test_outer_namespace +""" + +EXPECTED_HEADER_FILE = """\ +// Copyright 2016 Google Inc. All Rights Reserved. + +#ifndef TEST_HEADER_GUARD +#define TEST_HEADER_GUARD + +#include + +namespace test_outer_namespace { +namespace test_inner_namespace { + +extern const size_t test_array_name_size; +extern const unsigned char test_array_name[]; +extern const char test_fileid[]; + +} // namespace test_inner_namespace +} // namespace test_outer_namespace + +#endif // TEST_HEADER_GUARD +""" + +namespaces = ["test_outer_namespace", "test_inner_namespace"] +array_name = "test_array_name" +array_size_name = "test_array_name_size" +fileid = "test_fileid" +filename = "test_filename" +input_bytes = [1, 2, 3, 4, 5, 6, 7] +header_guard = "TEST_HEADER_GUARD" + + +class BinaryToArrayTest(googletest.TestCase): + + def test_source_file(self): + result_source = binary_to_array.source( + namespaces, array_name, array_size_name, fileid, filename, input_bytes) + self.assertEqual("\n".join(result_source), EXPECTED_SOURCE_FILE) + + def test_header_file(self): + result_header = binary_to_array.header(header_guard, namespaces, array_name, + array_size_name, fileid) + self.assertEqual("\n".join(result_header), EXPECTED_HEADER_FILE) + + +if __name__ == "__main__": + googletest.main() diff --git a/build_type_header_test.py b/build_type_header_test.py new file mode 100644 index 0000000000..45e8e775ef --- /dev/null +++ b/build_type_header_test.py @@ -0,0 +1,46 @@ +# Copyright 2018 Google LLC +# +# 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. + +"""Tests for google3.firebase.app.client.cpp.build_type_header.""" + +from google3.testing.pybase import googletest +from google3.firebase.app.client.cpp import build_type_header + +EXPECTED_BUILD_TYPE_HEADER = """\ +// Copyright 2017 Google Inc. All Rights Reserved. + +#ifndef FIREBASE_APP_CLIENT_CPP_SRC_BUILD_TYPE_H_ +#define FIREBASE_APP_CLIENT_CPP_SRC_BUILD_TYPE_H_ + +// Available build configurations for the suite of libraries. +#define FIREBASE_CPP_BUILD_TYPE_HEAD 0 +#define FIREBASE_CPP_BUILD_TYPE_STABLE 1 +#define FIREBASE_CPP_BUILD_TYPE_RELEASED 2 + +// Currently selected build type. +#define FIREBASE_CPP_BUILD_TYPE TEST_BUILD_TYPE + +#endif // FIREBASE_APP_CLIENT_CPP_SRC_BUILD_TYPE_H_ +""" + + +class BuildTypeHeaderTest(googletest.TestCase): + + def test_build_type_header(self): + result_header = build_type_header.generate_header('TEST_BUILD_TYPE') + self.assertEqual(result_header, EXPECTED_BUILD_TYPE_HEADER) + + +if __name__ == '__main__': + googletest.main() diff --git a/database/src/ios/util_ios_test.mm b/database/src/ios/util_ios_test.mm new file mode 100644 index 0000000000..ef0286f240 --- /dev/null +++ b/database/src/ios/util_ios_test.mm @@ -0,0 +1,111 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#import + +#include "app/src/util_ios.h" + +using ::firebase::Variant; +using ::firebase::util::VariantToId; + +@interface VariantToIdTests : XCTestCase +@end + +@implementation VariantToIdTests + +- (void)testNull { + XCTAssertEqual(VariantToId(Variant::Null()), [NSNull null]); +} + +- (void)testInt64WithZero { + id value_id = VariantToId(Variant::FromInt64(0LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 0LL); +} + +- (void)testInt64WithSigned32BitValue { + id value_id = VariantToId(Variant::FromInt64(2000000000LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 2000000000LL); +} + +- (void)testInt64WithLongLongValue { + id value_id = VariantToId(Variant::FromInt64(8000000000LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 8000000000LL); +} + +- (void)testInt64WithLargeValue { + id value_id = VariantToId(Variant::FromInt64(636900045569749380LL)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.longLongValue, 636900045569749380LL); +} + +- (void)testDoubleWithZeroPointZero { + id value_id = VariantToId(Variant::ZeroPointZero()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.doubleValue, 0.0); +} + +- (void)testDoubleWithOnePointZero { + id value_id = VariantToId(Variant::OnePointZero()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.doubleValue, 1.0); +} + +- (void)testDoubleWithPi { + id value_id = VariantToId(Variant::FromDouble(3.14159)); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.doubleValue, 3.14159); +} + +- (void)testBoolWithTrue { + id value_id = VariantToId(Variant::True()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.boolValue, true); +} + +- (void)testBoolWithFalse { + id value_id = VariantToId(Variant::False()); + NSNumber *value_number = (NSNumber *)value_id; + XCTAssertEqual(value_number.boolValue, false); +} + +- (void)testStaticStringWithEmptyString { + id value_id = VariantToId(Variant::FromStaticString("")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @""); +} + +- (void)testStaticStringWithNonEmptyString { + id value_id = VariantToId(Variant::FromStaticString("Hello, world!")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @"Hello, world!"); +} + +- (void)testMutableStringWithEmptyString { + id value_id = VariantToId(Variant::FromMutableString("")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @""); +} + +- (void)testMutableStringWithNonEmptyString { + id value_id = VariantToId(Variant::FromMutableString("Hello, world!")); + NSString *value_string = (NSString *)value_id; + XCTAssertEqualObjects(value_string, @"Hello, world!"); +} + +@end diff --git a/database/tests/CMakeLists.txt b/database/tests/CMakeLists.txt new file mode 100644 index 0000000000..5a8e416c23 --- /dev/null +++ b/database/tests/CMakeLists.txt @@ -0,0 +1,343 @@ +# Copyright 2019 Google LLC +# +# 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. + +firebase_cpp_cc_test( + firebase_rtdb_util_desktop_test + SOURCES + desktop/util_desktop_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_indexed_variant_test + SOURCES + desktop/core/indexed_variant_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_tracked_query_manager_test + SOURCES + desktop/core/tracked_query_manager_test.cc + desktop/test/mock_persistence_storage_engine.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_compound_write_test + SOURCES + desktop/core/compound_write_test.cc + DEPENDS + firebase_database + firebase_testing + +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_tree_test + SOURCES + desktop/core/tree_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_child_change_accumulator_test + SOURCES + desktop/view/child_change_accumulator_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_cache_test + SOURCES + desktop/view/view_cache_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_operation_test + SOURCES + desktop/core/operation_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_write_tree_test + SOURCES + desktop/core/write_tree_test.cc + desktop/test/mock_write_tree.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_indexed_filter_test + SOURCES + desktop/view/indexed_filter_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_limited_filter_test + SOURCES + desktop/view/limited_filter_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_ranged_filter_test + SOURCES + desktop/view/ranged_filter_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_test + SOURCES + desktop/test/matchers.h + desktop/view/view_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_matchers_test + SOURCES + desktop/test/matchers.h + desktop/test/matchers_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_processor_test + SOURCES + desktop/view/view_processor_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_persistence_manager_test + SOURCES + desktop/persistence/persistence_manager_test.cc + desktop/test/mock_cache_policy.h + desktop/test/mock_persistence_storage_engine.h + desktop/test/mock_tracked_query_manager.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_noop_persistence_manager_test + SOURCES + desktop/persistence/noop_persistence_manager_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_cache_policy_test + SOURCES + desktop/core/cache_policy_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_prune_forest_test + SOURCES + desktop/persistence/prune_forest_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_in_memory_persistence_storage_engine_test + SOURCES + desktop/persistence/in_memory_persistence_storage_engine_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_flatbuffer_conversion_test + SOURCES + desktop/persistence/flatbuffer_conversions_test.cc + DEPENDS + firebase_database + firebase_testing + flexbuffer_matcher +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_sync_point_test + SOURCES + desktop/core/sync_point_test.cc + desktop/test/matchers.h + desktop/test/mock_cache_policy.h + desktop/test/mock_listener.h + desktop/test/mock_persistence_manager.h + desktop/test/mock_persistence_storage_engine.h + desktop/test/mock_tracked_query_manager.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_sync_tree_test + SOURCES + desktop/core/sync_tree_test.cc + desktop/test/mock_listen_provider.h + desktop/test/mock_listener.h + desktop/test/mock_persistence_manager.h + desktop/test/mock_persistence_storage_engine.h + desktop/test/mock_tracked_query_manager.h + desktop/test/mock_write_tree.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_server_values_test + SOURCES + desktop/core/server_values_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_sparse_snapshot_tree_test + SOURCES + desktop/core/sparse_snapshot_tree_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_core_event_registration_test + SOURCES + desktop/core/event_registration_test.cc + desktop/test/mock_listener.h + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_mutable_data_desktop_test + SOURCES + desktop/mutable_data_desktop_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_change_test + SOURCES + desktop/view/change_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_view_event_generator_test + SOURCES + desktop/view/event_generator_test.cc + DEPENDS + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_common_database_reference_test + SOURCES + common/database_reference_test.cc + DEPENDS + firebase_app_for_testing + firebase_database + firebase_testing +) + +firebase_cpp_cc_test( + firebase_rtdb_desktop_connection_web_socket_client_impl_test + SOURCES + desktop/connection/web_socket_client_impl_test.cc + INCLUDES + ${OPENSSL_INCLUDE_DIR} + ${UWEBSOCKETS_SOURCE_DIR}/.. + DEPENDS + firebase_database + firebase_testing + ${OPENSSL_CRYPTO_LIBRARY} + libuWS +) + +if(MSVC) + target_compile_definitions(firebase_rtdb_desktop_connection_web_socket_client_impl_test + PRIVATE + -DWIN32_LEAN_AND_MEAN + ) +endif() + +firebase_cpp_cc_test( + firebase_rtdb_desktop_connection_connection_test + SOURCES + desktop/connection/connection_test.cc + INCLUDES + ${OPENSSL_INCLUDE_DIR} + ${UWEBSOCKETS_SOURCE_DIR}/.. + DEPENDS + ${OPENSSL_CRYPTO_DIR} + libuWS + firebase_app_for_testing + firebase_database + firebase_testing +) + diff --git a/database/tests/common/database_reference_test.cc b/database/tests/common/database_reference_test.cc new file mode 100644 index 0000000000..8d83547181 --- /dev/null +++ b/database/tests/common/database_reference_test.cc @@ -0,0 +1,283 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/include/firebase/database/database_reference.h" + +#include + +#include "app/src/include/firebase/app.h" +#include "app/src/thread.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/database_reference.h" +#include "database/src/include/firebase/database.h" + +using firebase::App; +using firebase::AppOptions; + +using testing::Eq; + +static const char kApiKey[] = "MyFakeApiKey"; +static const char kDatabaseUrl[] = "https://abc-xyz-123.firebaseio.com"; + +namespace firebase { +namespace database { + +class DatabaseReferenceTest : public ::testing::Test { + public: + void SetUp() override { + AppOptions options = testing::MockAppOptions(); + options.set_database_url(kDatabaseUrl); + options.set_api_key(kApiKey); + app_ = testing::CreateApp(options); + database_ = Database::GetInstance(app_); + } + + void DeleteDatabase() { + delete database_; + database_ = nullptr; + } + + void TearDown() override { + delete database_; + delete app_; + } + + protected: + App* app_; + Database* database_; +}; + +// Test DatabaseReference() +TEST_F(DatabaseReferenceTest, DefaultConstructor) { + DatabaseReference ref; + EXPECT_FALSE(ref.is_valid()); +} + +// Test DatabaseReference(DatabaseReferenceInternal*) +TEST_F(DatabaseReferenceTest, ConstructorWithInternalPointer) { + // Assume Database::GetReference() would utilize DatabaseReferenceInternal* + // created from different platform-dependent code to create + // DatabaseReference. + EXPECT_TRUE(database_->GetReference().is_valid()); + EXPECT_TRUE(database_->GetReference().is_root()); + EXPECT_THAT(database_->GetReference().key_string(), Eq("")); + + EXPECT_TRUE(database_->GetReference("child").is_valid()); + EXPECT_FALSE(database_->GetReference("child").is_root()); + EXPECT_THAT(database_->GetReference("child").key_string(), Eq("child")); +} + +// Test DatabaseReference(const DatabaseReference&) +TEST_F(DatabaseReferenceTest, CopyConstructor) { + DatabaseReference ref_null; + DatabaseReference ref_copy_null(ref_null); + EXPECT_FALSE(ref_copy_null.is_valid()); + + DatabaseReference ref_copy_root(database_->GetReference()); + EXPECT_TRUE(ref_copy_root.is_valid()); + EXPECT_TRUE(ref_copy_root.is_root()); + EXPECT_THAT(ref_copy_root.key_string(), Eq("")); + + DatabaseReference ref_copy_child(database_->GetReference("child")); + EXPECT_TRUE(ref_copy_child.is_valid()); + EXPECT_FALSE(ref_copy_child.is_root()); + EXPECT_THAT(ref_copy_child.key_string(), Eq("child")); +} + +// Test DatabaseReference(DatabaseReference&&) +TEST_F(DatabaseReferenceTest, MoveConstructor) { + DatabaseReference ref_null; + DatabaseReference ref_move_null(std::move(ref_null)); + EXPECT_FALSE(ref_move_null.is_valid()); + + DatabaseReference ref_root = database_->GetReference(); + DatabaseReference ref_move_root(std::move(ref_root)); + EXPECT_FALSE(ref_root.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_root.is_valid()); + EXPECT_TRUE(ref_move_root.is_root()); + EXPECT_THAT(ref_move_root.key_string(), Eq("")); + + DatabaseReference ref_child = database_->GetReference("child"); + DatabaseReference ref_move_child(std::move(ref_child)); + EXPECT_FALSE(ref_child.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_child.is_valid()); + EXPECT_FALSE(ref_move_child.is_root()); + EXPECT_THAT(ref_move_child.key_string(), Eq("child")); +} + +// Test operator=(const DatabaseReference&) +TEST_F(DatabaseReferenceTest, CopyOperator) { + DatabaseReference ref_copy_null; + ref_copy_null = DatabaseReference(); + EXPECT_FALSE(ref_copy_null.is_valid()); + + DatabaseReference ref_copy_root; + ref_copy_root = database_->GetReference(); + EXPECT_TRUE(ref_copy_root.is_valid()); + EXPECT_TRUE(ref_copy_root.is_root()); + EXPECT_THAT(ref_copy_root.key_string(), Eq("")); + + DatabaseReference ref_copy_child; + ref_copy_child = database_->GetReference("child"); + EXPECT_TRUE(ref_copy_child.is_valid()); + EXPECT_FALSE(ref_copy_child.is_root()); + EXPECT_THAT(ref_copy_child.key_string(), Eq("child")); +} + +// Test operator=(DatabaseReference&&) +TEST_F(DatabaseReferenceTest, MoveOperator) { + DatabaseReference ref_null; + DatabaseReference ref_move_null; + ref_move_null = std::move(ref_null); + EXPECT_FALSE(ref_move_null.is_valid()); + + DatabaseReference ref_root = database_->GetReference(); + DatabaseReference ref_move_root; + ref_move_root = std::move(ref_root); + EXPECT_FALSE(ref_root.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_root.is_valid()); + EXPECT_TRUE(ref_move_root.is_root()); + EXPECT_THAT(ref_move_root.key_string(), Eq("")); + + DatabaseReference ref_child = database_->GetReference("child"); + DatabaseReference ref_move_child; + ref_move_child = std::move(ref_child); + EXPECT_FALSE(ref_child.is_valid()); // NOLINT + EXPECT_TRUE(ref_move_child.is_valid()); + EXPECT_FALSE(ref_move_child.is_root()); + EXPECT_THAT(ref_move_child.key_string(), Eq("child")); +} + +TEST_F(DatabaseReferenceTest, CleanupFunction) { + // Reused temporary DatabaseReference to be move to another DatabaseReference + DatabaseReference ref_to_be_moved; + + // Null DatabaseReference created through default constructor, copy + // constructor, copy operator, move constructor and move operator + DatabaseReference ref_null; + DatabaseReference ref_copy_const_null(ref_null); + DatabaseReference ref_copy_op_null; + ref_copy_op_null = ref_null; + ref_to_be_moved = ref_null; + DatabaseReference ref_move_const_null(std::move(ref_to_be_moved)); + ref_to_be_moved = ref_null; + DatabaseReference ref_move_op_null; + ref_move_op_null = std::move(ref_to_be_moved); + + // Root DatabaseReference created through default constructor, copy + // constructor, copy operator, move constructor and move operator + DatabaseReference ref_root = database_->GetReference(); + DatabaseReference ref_copy_const_root(ref_root); + DatabaseReference ref_copy_op_root; + ref_copy_op_root = ref_root; + ref_to_be_moved = ref_root; + DatabaseReference ref_move_const_root(std::move(ref_to_be_moved)); + ref_to_be_moved = ref_root; + DatabaseReference ref_move_op_root; + ref_move_op_root = std::move(ref_to_be_moved); + + // Child DatabaseReference created through default constructor, copy + // constructor, copy operator, move constructor and move operator + DatabaseReference ref_child = database_->GetReference("child"); + DatabaseReference ref_copy_const_child(ref_child); + DatabaseReference ref_copy_op_child; + ref_copy_op_child = ref_child; + ref_to_be_moved = ref_child; + DatabaseReference ref_move_const_child(std::move(ref_to_be_moved)); + ref_to_be_moved = ref_child; + DatabaseReference ref_move_op_child; + ref_move_op_child = std::move(ref_to_be_moved); + + DeleteDatabase(); + + EXPECT_FALSE(ref_null.is_valid()); + EXPECT_FALSE(ref_copy_const_null.is_valid()); + EXPECT_FALSE(ref_copy_op_null.is_valid()); + EXPECT_FALSE(ref_move_const_null.is_valid()); + EXPECT_FALSE(ref_move_op_null.is_valid()); + + EXPECT_FALSE(ref_root.is_valid()); + EXPECT_FALSE(ref_copy_const_root.is_valid()); + EXPECT_FALSE(ref_copy_op_root.is_valid()); + EXPECT_FALSE(ref_move_const_root.is_valid()); + EXPECT_FALSE(ref_move_op_root.is_valid()); + + EXPECT_FALSE(ref_child.is_valid()); + EXPECT_FALSE(ref_copy_const_child.is_valid()); + EXPECT_FALSE(ref_copy_op_child.is_valid()); + EXPECT_FALSE(ref_move_const_child.is_valid()); + EXPECT_FALSE(ref_move_op_child.is_valid()); + + EXPECT_FALSE(ref_to_be_moved.is_valid()); // NOLINT +} + +// Ensure that creating and moving around DatabaseReferences in one thread while +// the Database is deleted from another thread still properly cleans up all +// DatabaseReferences. +TEST_F(DatabaseReferenceTest, RaceConditionTest) { + struct TestUserdata { + DatabaseReference ref_null; + DatabaseReference ref_root; + DatabaseReference ref_child; + }; + + const int kThreadCount = 100; + std::vector threads; + threads.reserve(kThreadCount); + + for (int i = 0; i < kThreadCount; i++) { + TestUserdata* userdata = new TestUserdata; + userdata->ref_root = database_->GetReference(); + userdata->ref_child = database_->GetReference("child"); + + threads.emplace_back( + [](void* void_userdata) { + TestUserdata* userdata = static_cast(void_userdata); + + // If the Database has not been deletd, these DatabaseReferences are + // valid. If the Database has been deleted, these DatabaseReferences + // should be automatically emptied. + // + // We don't know if the Database has been deleted or not yet (and thus + // whether these DatabaseReferences are empty or not), so there's not + // really any test we can do on them other than to ensure that calling + // various constructors on them doesn't crash. + DatabaseReference ref_move_null; + ref_move_null = std::move(userdata->ref_null); + (void)ref_move_null; + + DatabaseReference ref_move_root; + ref_move_root = std::move(userdata->ref_root); + (void)ref_move_root; + + DatabaseReference ref_move_child; + ref_move_child = std::move(userdata->ref_child); + (void)ref_move_child; + + delete userdata; + }, + userdata); + } + + DeleteDatabase(); + + for (Thread& t : threads) { + t.Join(); + } +} + +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/connection/connection_test.cc b/database/tests/desktop/connection/connection_test.cc new file mode 100644 index 0000000000..57359c8295 --- /dev/null +++ b/database/tests/desktop/connection/connection_test.cc @@ -0,0 +1,301 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/connection/connection.h" + +#include + +#include "app/src/include/firebase/app.h" +#include "app/src/scheduler.h" +#include "app/src/semaphore.h" +#include "app/src/time.h" +#include "app/src/variant_util.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +static const char kDatabaseHostname[] = "cpp-database-test-app.firebaseio.com"; +static const char kDatabaseNamespace[] = "cpp-database-test-app"; + +namespace firebase { +namespace database { +namespace internal { +namespace connection { + +class ConnectionTest : public ::testing::Test, public ConnectionEventHandler { + protected: + ConnectionTest() + : test_host_info_(nullptr), + sem_on_cache_host_(0), + sem_on_ready_(0), + sem_on_data_message_(0), + sem_on_disconnect_(0) {} + + void SetUp() override { + testing::CreateApp(); + test_host_info_ = new HostInfo(kDatabaseHostname, kDatabaseNamespace, true); + } + + void TearDown() override { + delete firebase::App::GetInstance(); + delete test_host_info_; + } + + void OnCacheHost(const std::string& host) override { + LogDebug("OnCacheHost: %s", host.c_str()); + sem_on_cache_host_.Post(); + } + + void OnReady(int64_t timestamp, const std::string& sessionId) override { + LogDebug("OnReady: %lld, %s", timestamp, sessionId.c_str()); + last_session_id_ = sessionId; + sem_on_ready_.Post(); + } + + void OnDataMessage(const Variant& data) override { + LogDebug("OnDataMessage: %s", util::VariantToJson(data).c_str()); + sem_on_data_message_.Post(); + } + + void OnDisconnect(Connection::DisconnectReason reason) override { + LogDebug("OnDisconnect: %d", static_cast(reason)); + sem_on_disconnect_.Post(); + } + + void OnKill(const std::string& reason) override { + LogDebug("OnKill: %s", reason.c_str()); + } + + void ScheduledOpen(Connection* connection) { + scheduler_.Schedule(new callback::CallbackValue1( + connection, [](Connection* connection) { connection->Open(); })); + } + + void ScheduledSend(Connection* connection, const Variant& message) { + scheduler_.Schedule(new callback::CallbackValue2( + connection, message, [](Connection* connection, Variant message) { + connection->Send(message, false); + })); + } + + void ScheduledClose(Connection* connection) { + scheduler_.Schedule(new callback::CallbackValue1( + connection, [](Connection* connection) { connection->Close(); })); + } + + HostInfo GetHostInfo() { + assert(test_host_info_ != nullptr); + if (test_host_info_) { + return *test_host_info_; + } else { + return HostInfo(); + } + } + + scheduler::Scheduler scheduler_; + + HostInfo* test_host_info_; + + std::string last_session_id_; + + Semaphore sem_on_cache_host_; + Semaphore sem_on_ready_; + Semaphore sem_on_data_message_; + Semaphore sem_on_disconnect_; +}; + +static const int kTimeoutMs = 5000; + +TEST_F(ConnectionTest, DeleteConnectionImmediately) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); +} + +TEST_F(ConnectionTest, OpenConnection) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, CloseConnection) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + ScheduledClose(&connection); + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, MultipleConnections) { + const int kNumOfConnections = 10; + std::vector connections; + + Logger logger(nullptr); + for (int i = 0; i < kNumOfConnections; ++i) { + connections.push_back( + new Connection(&scheduler_, GetHostInfo(), nullptr, this, &logger)); + } + + for (auto& itConnection : connections) { + ScheduledOpen(itConnection); + } + + for (int i = 0; i < connections.size(); ++i) { + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + } + + for (auto& itConnection : connections) { + ScheduledClose(itConnection); + } + + for (int i = 0; i < connections.size(); ++i) { + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); + } + + for (int i = 0; i < kNumOfConnections; ++i) { + delete connections[i]; + connections[i] = nullptr; + } +} + +TEST_F(ConnectionTest, LastSession) { + Logger logger(nullptr); + Connection connection1(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection1); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + Connection connection2(&scheduler_, GetHostInfo(), last_session_id_.c_str(), + this, &logger); + + ScheduledOpen(&connection2); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + // connection1 disconnected + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); + + ScheduledClose(&connection2); + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); +} + +const char* const kWireProtocolClearRoot = + "{\"r\":1,\"a\":\"p\",\"b\":{\"p\":\"/connection/ConnectionTest/" + "\",\"d\": null}}"; +const char* const kWireProtocolListenRoot = + "{\"r\":2,\"a\":\"q\",\"b\":{\"p\":\"/connection/ConnectionTest/" + "\",\"h\":\"\"}}"; + +TEST_F(ConnectionTest, SimplePutRequest) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + ScheduledSend(&connection, util::JsonToVariant(kWireProtocolClearRoot)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, LargeMessage) { + Logger logger(nullptr); + Connection connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + + ScheduledOpen(&connection); + EXPECT_TRUE(sem_on_cache_host_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_ready_.TimedWait(kTimeoutMs)); + + ScheduledSend(&connection, util::JsonToVariant(kWireProtocolClearRoot)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); + + ScheduledSend(&connection, util::JsonToVariant(kWireProtocolListenRoot)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); + + // Send a long message + std::stringstream ss; + ss << "{\"r\":3,\"a\":\"p\",\"b\":{\"p\":\"/connection/ConnectionTest/" + "\",\"d\":\""; + for (int i = 0; i < 20000; ++i) { + ss << "!"; + } + ss << "\"}}"; + + ScheduledSend(&connection, util::JsonToVariant(ss.str().c_str())); + + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); + EXPECT_TRUE(sem_on_data_message_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, TestBadHost) { + HostInfo bad_host("bad-host-name.bad", "bad-namespace", true); + Logger logger(nullptr); + Connection connection(&scheduler_, bad_host, nullptr, this, &logger); + connection.Open(); + EXPECT_TRUE(sem_on_disconnect_.TimedWait(kTimeoutMs)); +} + +TEST_F(ConnectionTest, TestCreateDestroyRace) { + Logger logger(nullptr); + // Test race when connecting to a valid host without sleep + // Try this on real server less time or the server may block this client + for (int i = 0; i < 10; ++i) { + Connection* connection = + new Connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + connection->Open(); + delete connection; + } + + // Test race when connecting to a valid host with sleep, to wait for websocket + // thread to kick-in + for (int i = 0; i < 10; ++i) { + Connection* connection = + new Connection(&scheduler_, GetHostInfo(), nullptr, this, &logger); + connection->Open(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + delete connection; + } + + // Test race when connecting to a bad host name without sleep + HostInfo bad_host("bad-host-name.bad", "bad-namespace", true); + for (int i = 0; i < 100; ++i) { + Connection* connection = + new Connection(&scheduler_, bad_host, nullptr, this, &logger); + connection->Open(); + delete connection; + } + + // Test race when connecting to a bad host name with sleep, to wait for + // websocket thread to kick-in + for (int i = 0; i < 100; ++i) { + Connection* connection = + new Connection(&scheduler_, bad_host, nullptr, this, &logger); + connection->Open(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + delete connection; + } +} + +} // namespace connection +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/connection/web_socket_client_impl_test.cc b/database/tests/desktop/connection/web_socket_client_impl_test.cc new file mode 100644 index 0000000000..a79fcc8ca4 --- /dev/null +++ b/database/tests/desktop/connection/web_socket_client_impl_test.cc @@ -0,0 +1,268 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/connection/web_socket_client_impl.h" +#include "app/src/semaphore.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace connection { + +// Simple WebSocket based Echo Server using third_party/uWebSockets +// It has some quirk. Ex. hub_ needs a handler (async_) to wake the loop before +// closing it or the event loop will never stop. +class TestWebSocketEchoServer { + public: + explicit TestWebSocketEchoServer(int port) + : port_(port), run_(false), thread_(nullptr), keep_alive_(nullptr) { + hub_.onMessage([](uWS::WebSocket* ws, char* message, + size_t length, uWS::OpCode opCode) { + // Echo back immediately + ws->send(message, length, opCode); + }); + hub_.onConnection( + [](uWS::WebSocket* ws, uWS::HttpRequest request) { + LogDebug("[Server] Received connection from (%s) %s port: %d", + ws->getAddress().family, ws->getAddress().address, + ws->getAddress().port); + }); + hub_.onDisconnection([](uWS::WebSocket* ws, int code, + char* message, size_t length) { + LogDebug("[Server] Disconnected from (%s) %s port: %d", + ws->getAddress().family, ws->getAddress().address, + ws->getAddress().port); + }); + } + + ~TestWebSocketEchoServer() { Stop(); } + + void Start() { + keep_alive_ = new uS::Async(hub_.getLoop()); + keep_alive_->setData(this); + keep_alive_->start([](uS::Async* async) { + TestWebSocketEchoServer* server = + static_cast(async->getData()); + assert(server != nullptr); + // close ths group in event loop thread + server->hub_.getDefaultGroup().close(); + async->close(); + }); + + run_ = true; + thread_ = new std::thread([this]() { + auto listen = [&](int port){ + if (hub_.listen(port)) { + LogDebug("[Server] Starts to listen to port %d", port); + return true; + } else { + LogDebug("[Server] Cannot listen to port %d", port); + return false; + } + }; + + if (port_ == 0) { + int attempts = 1000; + int port = 0; + bool res = false; + + do { + --attempts; + port = 10000 + (rand() % 55000); // NOLINT + res = listen(port); + } while (run_ == true && res == false && attempts != 0); + + if (res) { + port_ = port; + hub_.run(); // Blocks until done + } else if (attempts == 0) { + LogError("Failed to find free port after 1000 attempts"); + } + } else { + if (listen(port_) == true) { + hub_.run(); // Blocks until done + } else { + LogWarning("[Server] Cannot listen to port %d", port_.load()); + } + } + + run_ = false; + }); + } + + void Stop() { + run_ = false; + + if (keep_alive_) { + keep_alive_->send(); + keep_alive_ = nullptr; + } + + if (thread_ != nullptr) { + thread_->join(); + delete thread_; + thread_ = nullptr; + } + } + + int GetPort(bool waitForPort = false) const { + while (waitForPort == true && run_ == true && port_ == 0) { + firebase::internal::Sleep(10); + } + + return port_; + } + + private: + std::atomic port_; + std::atomic run_; // Is the listen thread started and running + uWS::Hub hub_; + std::thread* thread_; + uS::Async* keep_alive_; +}; + +std::string GetLocalHostUri(int port) { + std::stringstream ss; + ss << "ws://localhost:" << port; + return ss.str(); +} + +class TestClientEventHandler : public WebSocketClientEventHandler { + public: + explicit TestClientEventHandler(Semaphore* s) + : is_connected_(false), + is_msg_received_(false), + msg_received_(), + is_closed_(false), + is_error_(false), + semaphore_(s) {} + ~TestClientEventHandler() override{}; + + void OnOpen() override { + is_connected_ = true; + semaphore_->Post(); + } + + void OnMessage(const char* msg) override { + is_msg_received_ = true; + msg_received_ = msg; + semaphore_->Post(); + } + + void OnClose() override { + is_closed_ = true; + semaphore_->Post(); + } + + void OnError(const WebSocketClientErrorData& error_data) override { + is_error_ = true; + semaphore_->Post(); + } + + bool is_connected_ = false; + bool is_msg_received_ = false; + std::string msg_received_; + bool is_closed_ = false; + bool is_error_ = false; + + private: + Semaphore* semaphore_; +}; + +// Test if the client can connect to a local echo server, send a message, +// receive message and close the connection properly. +TEST(WebSocketClientImpl, Test1) { + // Launch a local echo server + TestWebSocketEchoServer server(0); + server.Start(); + + auto uri = GetLocalHostUri(server.GetPort(true)); + + Semaphore semaphore(1); + TestClientEventHandler handler(&semaphore); + Logger logger(nullptr); + scheduler::Scheduler scheduler; + WebSocketClientImpl ws_client(uri.c_str(), "", &logger, &scheduler, &handler); + + // Connect to local server + LogDebug("[Client] Connecting to %s", uri.c_str()); + EXPECT_TRUE(semaphore.TryWait()); + ws_client.Connect(5000); + semaphore.Wait(); + semaphore.Post(); + EXPECT_TRUE(handler.is_connected_ && !handler.is_error_); + + // Send a message and wait for the response + EXPECT_TRUE(semaphore.TryWait()); + ws_client.Send("Hello World"); + semaphore.Wait(); + semaphore.Post(); + EXPECT_TRUE(handler.is_msg_received_ && !handler.is_error_); + EXPECT_STREQ("Hello World", handler.msg_received_.c_str()); + + // Close the connection + EXPECT_TRUE(semaphore.TryWait()); + ws_client.Close(); + semaphore.Wait(); + semaphore.Post(); + EXPECT_TRUE(handler.is_closed_ && !handler.is_error_); + + // Stop the server + server.Stop(); +} + +// Test if it is safe to create the client and destroy it immediately. +// This is to test if the destructor can properly end the event loop. +// Otherwise, it would block forever and timeout +TEST(WebSocketClientImpl, TestEdgeCase1) { + Logger logger(nullptr); + scheduler::Scheduler scheduler; + WebSocketClientImpl ws_client("ws://localhost", "", &logger, &scheduler); +} + +// Test if it is safe to connect to a server and destroy the client immediately. +// This is to test if the destructor can properly end the event loop +// Otherwise, it would block forever and timeout +TEST(WebSocketClientImpl, TestEdgeCase2) { + // Launch a local echo server + TestWebSocketEchoServer server(0); + server.Start(); + Logger logger(nullptr); + scheduler::Scheduler scheduler; + + auto uri = GetLocalHostUri(server.GetPort(true)); + + int count = 0; + while ((count++) < 10000) { + WebSocketClientImpl* ws_client = + new WebSocketClientImpl(uri.c_str(), "", &logger, &scheduler); + + // Connect to local server + LogDebug("[Client][%d] Connecting to %s", count, uri.c_str()); + ws_client->Connect(5000); + + // Immediately destroy the client right after connect request + delete ws_client; + } + + // Stop the server + server.Stop(); +} + +} // namespace connection +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/cache_policy_test.cc b/database/tests/desktop/core/cache_policy_test.cc new file mode 100644 index 0000000000..a21e8d5493 --- /dev/null +++ b/database/tests/desktop/core/cache_policy_test.cc @@ -0,0 +1,70 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/cache_policy.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +namespace { + +TEST(LRUCachePolicy, ShouldPrune) { + const uint64_t kMaxSizeBytes = 1000; + LRUCachePolicy cache_policy(kMaxSizeBytes); + + uint64_t queries_to_keep = cache_policy.GetMaxNumberOfQueriesToKeep(); + EXPECT_EQ(queries_to_keep, 1000); + + // Should prune if the current number of bytes exceeds the max number of + // bytes. + EXPECT_TRUE(cache_policy.ShouldPrune(2000, 0)); + // Should prune if the number of prunable queries is greater than the maximum + // number of prunable queries (defined in the LRUCachePolicy implementation). + EXPECT_TRUE(cache_policy.ShouldPrune(0, 2000)); + // Should prune if both of the above are true. + EXPECT_TRUE(cache_policy.ShouldPrune(2000, 2000)); + + // Should not prune if at least one of the above conditions is not met. + EXPECT_FALSE(cache_policy.ShouldPrune(0, 0)); +} + +TEST(LRUCachePolicy, ShouldCheckCacheSize) { + const uint64_t kMaxSizeBytes = 1000; + LRUCachePolicy cache_policy(kMaxSizeBytes); + + // Should check cache cize of the number of server updates is greater than + // number of server updates between cache checks (defined in the + // LRUCachePolicy implementation). + EXPECT_TRUE(cache_policy.ShouldCheckCacheSize(2000)); + EXPECT_TRUE(cache_policy.ShouldCheckCacheSize(1001)); + EXPECT_FALSE(cache_policy.ShouldCheckCacheSize(1000)); + EXPECT_FALSE(cache_policy.ShouldCheckCacheSize(500)); +} + +TEST(LRUCachePolicy, GetPercentOfQueriesToPruneAtOnce) { + const uint64_t kMaxSizeBytes = 1000; + LRUCachePolicy cache_policy(kMaxSizeBytes); + + // This should be exactly 20%. + EXPECT_EQ(cache_policy.GetPercentOfQueriesToPruneAtOnce(), .2); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/compound_write_test.cc b/database/tests/desktop/core/compound_write_test.cc new file mode 100644 index 0000000000..435f2b7291 --- /dev/null +++ b/database/tests/desktop/core/compound_write_test.cc @@ -0,0 +1,545 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/compound_write.h" + +#include +#include + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(CompoundWrite, CompoundWrite) { + { + CompoundWrite write; + EXPECT_TRUE(write.IsEmpty()); + EXPECT_TRUE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.GetRootWrite().has_value()); + } + { + CompoundWrite write = CompoundWrite::EmptyWrite(); + EXPECT_TRUE(write.IsEmpty()); + EXPECT_TRUE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.GetRootWrite().has_value()); + } +} + +TEST(CompoundWrite, FromChildMerge) { + { + const std::map& merge{ + std::make_pair("", 0), + }; + CompoundWrite write = CompoundWrite::FromChildMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_EQ(*write.GetRootWrite(), 0); + } + { + const std::map& merge{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc/ddd", 3), + std::make_pair("ccc/eee", 4), + }; + CompoundWrite write = CompoundWrite::FromChildMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.write_tree().value().has_value()); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(write.write_tree().GetValueAt(Path("zzz")), nullptr); + } +} + +TEST(CompoundWrite, FromVariantMerge) { + { + Variant merge(std::map{ + std::make_pair("", 0), + }); + CompoundWrite write = CompoundWrite::FromVariantMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_EQ(*write.GetRootWrite(), 0); + } + { + Variant merge(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc/ddd", 3), + std::make_pair("ccc/eee", 4), + }); + CompoundWrite write = CompoundWrite::FromVariantMerge(merge); + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.write_tree().value().has_value()); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(write.write_tree().GetValueAt(Path("zzz")), nullptr); + } +} + +TEST(CompoundWrite, FromPathMerge) { + { + const std::map& merge{ + std::make_pair(Path(""), 0), + }; + + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_EQ(*write.GetRootWrite(), 0); + } + { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_FALSE(write.IsEmpty()); + EXPECT_FALSE(write.write_tree().IsEmpty()); + EXPECT_FALSE(write.write_tree().value().has_value()); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(write.write_tree().GetValueAt(Path("zzz")), nullptr); + } +} + +// This just replicates the set up work done in the FromPathMerge test. +class CompoundWriteTest : public ::testing::Test { + void SetUp() override { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })), + }; + + write_ = CompoundWrite::FromPathMerge(merge); + } + + void TearDown() override {} + + protected: + CompoundWrite write_; +}; + +TEST_F(CompoundWriteTest, EmptyWrite) { + CompoundWrite empty = CompoundWrite::EmptyWrite(); + EXPECT_TRUE(empty.IsEmpty()); +} + +TEST_F(CompoundWriteTest, AddWriteEmptyPath) { + CompoundWrite new_write = write_.AddWrite(Path(), Optional(100)); + + // New write should just be the root value. + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("bbb")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/ddd")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/eee")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/fff")), nullptr); + EXPECT_TRUE(new_write.write_tree().value().has_value()); + EXPECT_EQ(new_write.write_tree().value().value(), 100); +} + +TEST_F(CompoundWriteTest, AddWriteInlineEmptyPath) { + write_.AddWriteInline(Path(), Optional(100)); + CompoundWrite& new_write = write_; + + // New write should just be the root value. + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("bbb")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/ddd")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/eee")), nullptr); + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("ccc/fff")), nullptr); + EXPECT_TRUE(new_write.write_tree().value().has_value()); + EXPECT_EQ(new_write.write_tree().value().value(), 100); +} + +TEST_F(CompoundWriteTest, AddWritePriorityWrite) { + { + CompoundWrite new_write = + write_.AddWrite(Path("ccc/.priority"), Optional(100)); + + // Everything should be the same, but with an additional .priority field. + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/fff")), + Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/.priority")), 100); + } + + { + CompoundWrite new_write = + write_.AddWrite(Path("aaa/bad_path/.priority"), Optional(100)); + + // New write should be identical to the old write. + EXPECT_EQ(new_write, write_); + } +} + +TEST_F(CompoundWriteTest, AddWriteThatDoesNotOverwrite) { + CompoundWrite new_write = + write_.AddWrite(Path("iii/jjj"), Optional(100)); + + // New write should have the new value alongside old values. + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/fff")), + Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("iii/jjj")), 100); +} + +TEST_F(CompoundWriteTest, AddWriteThatShadowsExistingData) { + CompoundWrite new_write = + write_.AddWrite(Path("ccc/fff/ggg"), Optional(100)); + + // Values being shadowed are still part of the CompoundWrite. + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/fff")), + Variant(std::map{ + std::make_pair("ggg", 100), + std::make_pair("hhh", 6), + })); +} + +TEST_F(CompoundWriteTest, AddWrites) { + const std::map& second_merge{ + std::make_pair(Path("zzz"), -1), + std::make_pair(Path("yyy"), -2), + std::make_pair(Path("xxx/www"), -3), + std::make_pair(Path("xxx/vvv"), -4), + }; + CompoundWrite second_write = CompoundWrite::FromPathMerge(second_merge); + + const std::map& third_merge{ + std::make_pair(Path("apple"), 1111), + std::make_pair(Path("banana"), 2222), + std::make_pair(Path("carrot/date"), 3333), + std::make_pair(Path("carrot/eggplant"), 4444), + }; + CompoundWrite third_write = CompoundWrite::FromPathMerge(third_merge); + + CompoundWrite updated_write = write_.AddWrites(Path(), second_write); + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); + + updated_write = updated_write.AddWrites(Path("ccc"), third_write); + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/apple")), 1111); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/banana")), 2222); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/date")), + 3333); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/eggplant")), + 4444); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); +} + +TEST_F(CompoundWriteTest, AddWritesInline) { + const std::map& second_merge{ + std::make_pair(Path("zzz"), -1), + std::make_pair(Path("yyy"), -2), + std::make_pair(Path("xxx/www"), -3), + std::make_pair(Path("xxx/vvv"), -4), + }; + CompoundWrite second_write = CompoundWrite::FromPathMerge(second_merge); + + const std::map& third_merge{ + std::make_pair(Path("apple"), 1111), + std::make_pair(Path("banana"), 2222), + std::make_pair(Path("carrot/date"), 3333), + std::make_pair(Path("carrot/eggplant"), 4444), + }; + CompoundWrite third_write = CompoundWrite::FromPathMerge(third_merge); + + write_.AddWritesInline(Path(), second_write); + CompoundWrite& updated_write = write_; + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); + + updated_write.AddWritesInline(Path("ccc"), third_write); + + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/apple")), 1111); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/banana")), 2222); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/date")), + 3333); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("ccc/carrot/eggplant")), + 4444); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("zzz")), -1); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("yyy")), -2); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/www")), -3); + EXPECT_EQ(*updated_write.write_tree().GetValueAt(Path("xxx/vvv")), -4); +} + +TEST_F(CompoundWriteTest, RemoveWrite) { + CompoundWrite new_write = write_.RemoveWrite(Path("aaa")); + + // New write should be missing aaa + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); +} + +TEST_F(CompoundWriteTest, RemoveWriteInline) { + write_.RemoveWriteInline(Path("aaa")); + CompoundWrite& new_write = write_; + + // New write should be missing aaa + EXPECT_EQ(new_write.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*new_write.write_tree().GetValueAt(Path("ccc/eee")), 4); +} + +TEST_F(CompoundWriteTest, HasCompleteWrite) { + EXPECT_TRUE(write_.HasCompleteWrite(Path("aaa"))); + EXPECT_TRUE(write_.HasCompleteWrite(Path("bbb"))); + EXPECT_FALSE(write_.HasCompleteWrite(Path("ccc"))); + EXPECT_TRUE(write_.HasCompleteWrite(Path("ccc/ddd"))); + EXPECT_TRUE(write_.HasCompleteWrite(Path("ccc/eee"))); + EXPECT_FALSE(write_.HasCompleteWrite(Path("zzz"))); +} + +TEST_F(CompoundWriteTest, GetRootWriteEmpty) { + Optional root = write_.GetRootWrite(); + EXPECT_FALSE(root.has_value()); +} + +TEST(CompoundWrite, GetRootWritePopulated) { + const std::map& merge{ + std::make_pair(Path(""), "One billion"), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + Optional root = write.GetRootWrite(); + EXPECT_TRUE(root.has_value()); + EXPECT_EQ(root.value(), "One billion"); +} + +TEST_F(CompoundWriteTest, GetCompleteVariant) { + EXPECT_FALSE(write_.GetCompleteVariant(Path()).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("aaa")).value(), 1); + EXPECT_EQ(write_.GetCompleteVariant(Path("bbb")).value(), 2); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/ddd")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/ddd")).value(), 3); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/eee")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/eee")).value(), 4); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/fff/ggg")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/fff/ggg")).value(), 5); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/fff/ggg")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/fff/hhh")).value(), 6); + EXPECT_TRUE(write_.GetCompleteVariant(Path("ccc/fff/iii")).has_value()); + EXPECT_EQ(write_.GetCompleteVariant(Path("ccc/fff/iii")).value(), + Variant::Null()); + EXPECT_FALSE(write_.GetCompleteVariant(Path("zzz")).has_value()); +} + +TEST_F(CompoundWriteTest, GetCompleteChildren) { + std::vector> children = + write_.GetCompleteChildren(); + + std::vector> expected_children = { + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + }; + + EXPECT_THAT(children, Pointwise(Eq(), expected_children)); +} + +TEST_F(CompoundWriteTest, ChildCompoundWriteEmptyPath) { + CompoundWrite child = write_.ChildCompoundWrite(Path()); + + // Should be exactly the same as write_. + EXPECT_FALSE(child.IsEmpty()); + EXPECT_FALSE(child.write_tree().IsEmpty()); + EXPECT_FALSE(child.write_tree().value().has_value()); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(child.write_tree().GetValueAt(Path("ccc")), nullptr); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(child.write_tree().GetValueAt(Path("zzz")), nullptr); +} + +TEST(CompoundWrite, ChildCompoundWriteShadowingWrite) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), -9999), std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + CompoundWrite child = write.ChildCompoundWrite(Path("ccc")); + EXPECT_EQ(child.GetRootWrite().value(), -9999); +} + +TEST_F(CompoundWriteTest, ChildCompoundWriteNonShadowingWrite) { + CompoundWrite child = write_.ChildCompoundWrite(Path("ccc")); + + EXPECT_FALSE(child.IsEmpty()); + EXPECT_FALSE(child.write_tree().IsEmpty()); + EXPECT_FALSE(child.write_tree().value().has_value()); + EXPECT_EQ(child.write_tree().GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(child.write_tree().GetValueAt(Path("bbb")), nullptr); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("ddd")), 3); + EXPECT_EQ(*child.write_tree().GetValueAt(Path("eee")), 4); + EXPECT_EQ(child.write_tree().GetValueAt(Path("zzz")), nullptr); +} + +TEST_F(CompoundWriteTest, ChildCompoundWrites) { + std::map writes = write_.ChildCompoundWrites(); + + CompoundWrite& aaa = writes["aaa"]; + CompoundWrite& bbb = writes["bbb"]; + CompoundWrite& ccc = writes["ccc"]; + + EXPECT_EQ(writes.size(), 3); + EXPECT_EQ(aaa.write_tree().value().value(), 1); + EXPECT_EQ(bbb.write_tree().value().value(), 2); + EXPECT_EQ(*ccc.write_tree().GetValueAt(Path("ddd")), 3); + EXPECT_EQ(*ccc.write_tree().GetValueAt(Path("eee")), 4); +} + +TEST_F(CompoundWriteTest, IsEmpty) { + CompoundWrite compound_write; + EXPECT_TRUE(compound_write.IsEmpty()); + + CompoundWrite empty = CompoundWrite::EmptyWrite(); + EXPECT_TRUE(empty.IsEmpty()); + + CompoundWrite add_write = compound_write.AddWrite(Path(), 100); + EXPECT_TRUE(compound_write.IsEmpty()); + EXPECT_FALSE(add_write.IsEmpty()); +} + +TEST_F(CompoundWriteTest, Apply) { + Variant expected_variant(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + }), + }), + std::make_pair("zzz", 100), + }); + Variant variant_to_apply(std::map{ + std::make_pair("zzz", 100), + }); + + EXPECT_EQ(write_.Apply(variant_to_apply), expected_variant); +} + +TEST_F(CompoundWriteTest, Equality) { + const std::map& same_merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })), + }; + CompoundWrite same_write = CompoundWrite::FromPathMerge(same_merge); + const std::map& different_merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 100), + })), + }; + CompoundWrite different_write = CompoundWrite::FromPathMerge(different_merge); + + EXPECT_EQ(write_, same_write); + EXPECT_NE(write_, different_write); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/event_registration_test.cc b/database/tests/desktop/core/event_registration_test.cc new file mode 100644 index 0000000000..45140db71e --- /dev/null +++ b/database/tests/desktop/core/event_registration_test.cc @@ -0,0 +1,186 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/event_registration.h" + +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/desktop/database_desktop.h" +#include "database/src/desktop/database_reference_desktop.h" +#include "database/src/desktop/view/change.h" +#include "database/src/desktop/view/event.h" +#include "database/src/desktop/view/event_type.h" +#include "database/src/include/firebase/database/common.h" +#include "database/tests/desktop/test/mock_listener.h" +#include "firebase/database/common.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::_; +using ::testing::StrEq; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(ValueEventRegistrationTest, RespondsTo) { + ValueEventRegistration registration(nullptr, nullptr, QuerySpec()); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildRemoved)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildAdded)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildMoved)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeChildChanged)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeValue)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeError)); +} + +TEST(ValueEventRegistrationTest, CreateEvent) { + ValueEventRegistration registration(nullptr, nullptr, QuerySpec()); + Variant variant = std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 200), + }; + IndexedVariant change_variant(variant, QueryParams()); + Change change(kEventTypeValue, change_variant, "new"); + QuerySpec query_spec; + query_spec.path = Path("change/path"); + Event event = registration.GenerateEvent(change, query_spec); + EXPECT_EQ(event.type, kEventTypeValue); + EXPECT_EQ(event.event_registration, ®istration); + EXPECT_EQ(event.snapshot->GetValue().int64_value(), 100); + EXPECT_EQ(event.snapshot->GetPriority().int64_value(), 200); + EXPECT_EQ(event.snapshot->path(), Path("change/path/new")); + EXPECT_STREQ(event.prev_name.c_str(), ""); + EXPECT_EQ(event.error, kErrorNone); + EXPECT_EQ(event.path, Path()); +} + +TEST(ValueEventRegistrationTest, FireEvent) { + MockValueListener listener; + ValueEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeValue, ®istration, snapshot); + EXPECT_CALL(listener, OnValueChanged(_)); + registration.FireEvent(event); +} + +TEST(ValueEventRegistrationTest, FireEventCancel) { + MockValueListener listener; + ValueEventRegistration registration(nullptr, &listener, QuerySpec()); + EXPECT_CALL(listener, OnCancelled(kErrorDisconnected, _)); + registration.FireCancelEvent(kErrorDisconnected); +} + +TEST(ValueEventRegistrationTest, MatchesListener) { + MockValueListener right_listener; + MockValueListener wrong_listener; + MockChildListener wrong_type_listener; + ValueEventRegistration registration(nullptr, &right_listener, QuerySpec()); + EXPECT_TRUE(registration.MatchesListener(&right_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_type_listener)); +} + +TEST(ChildEventRegistrationTest, RespondsTo) { + ChildEventRegistration registration(nullptr, nullptr, QuerySpec()); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildRemoved)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildAdded)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildMoved)); + EXPECT_TRUE(registration.RespondsTo(kEventTypeChildChanged)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeValue)); + EXPECT_FALSE(registration.RespondsTo(kEventTypeError)); +} + +TEST(ChildEventRegistrationTest, CreateEvent) { + ChildEventRegistration registration(nullptr, nullptr, QuerySpec()); + Variant variant = std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 200), + }; + IndexedVariant change_variant(variant, QueryParams()); + Change change(kEventTypeChildAdded, change_variant, "new"); + QuerySpec query_spec; + query_spec.path = Path("change/path"); + Event event = registration.GenerateEvent(change, query_spec); + EXPECT_EQ(event.type, kEventTypeChildAdded); + EXPECT_EQ(event.event_registration, ®istration); + EXPECT_EQ(event.snapshot->GetValue().int64_value(), 100); + EXPECT_EQ(event.snapshot->GetPriority().int64_value(), 200); + EXPECT_EQ(event.snapshot->path(), Path("change/path/new")); + EXPECT_STREQ(event.prev_name.c_str(), ""); + EXPECT_EQ(event.error, kErrorNone); + EXPECT_EQ(event.path, Path()); +} + +TEST(ChildEventRegistrationTest, FireChildAddedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildAdded, ®istration, snapshot, + "Apples and bananas"); + EXPECT_CALL(listener, OnChildAdded(_, StrEq("Apples and bananas"))); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireChildChangedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildChanged, ®istration, snapshot, + "Upples and banunus"); + EXPECT_CALL(listener, OnChildChanged(_, StrEq("Upples and banunus"))); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireChildMovedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildMoved, ®istration, snapshot, + "Epples and banenes"); + EXPECT_CALL(listener, OnChildMoved(_, StrEq("Epples and banenes"))); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireChildRemovedEvent) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + DataSnapshotInternal snapshot(nullptr, Variant(), QuerySpec()); + Event event(kEventTypeChildRemoved, ®istration, snapshot); + EXPECT_CALL(listener, OnChildRemoved(_)); + registration.FireEvent(event); +} + +TEST(ChildEventRegistrationTest, FireEventCancel) { + MockChildListener listener; + ChildEventRegistration registration(nullptr, &listener, QuerySpec()); + EXPECT_CALL(listener, OnCancelled(kErrorDisconnected, _)); + registration.FireCancelEvent(kErrorDisconnected); +} + +TEST(ChildEventRegistrationTest, MatchesListener) { + MockChildListener right_listener; + MockChildListener wrong_listener; + MockValueListener wrong_type_listener; + ChildEventRegistration registration(nullptr, &right_listener, QuerySpec()); + EXPECT_TRUE(registration.MatchesListener(&right_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_listener)); + EXPECT_FALSE(registration.MatchesListener(&wrong_type_listener)); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/indexed_variant_test.cc b/database/tests/desktop/core/indexed_variant_test.cc new file mode 100644 index 0000000000..5da86b9ba2 --- /dev/null +++ b/database/tests/desktop/core/indexed_variant_test.cc @@ -0,0 +1,677 @@ +// Copyright 2018 Google LLC +// +// 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 + +#include "app/memory/unique_ptr.h" +#include "app/src/variant_util.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "database/src/desktop/util_desktop.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::Ne; + +namespace firebase { +namespace database { +namespace internal { + +typedef std::vector> TestList; + +// Hardcoded Json string for test uses \' instead of \" for readability. +// This utility function converts \' into \" +std::string& ConvertQuote(std::string* in) { + std::replace(in->begin(), in->end(), '\'', '\"'); + return *in; +} + +// Test for ConvertQuote +TEST(IndexedVariantHelperFunction, ConvertQuote) { + { + std::string test_string = ""; + EXPECT_THAT(ConvertQuote(&test_string), Eq("")); + } + { + std::string test_string = "'"; + EXPECT_THAT(ConvertQuote(&test_string), Eq("\"")); + } + { + std::string test_string = "\""; + EXPECT_THAT(ConvertQuote(&test_string), Eq("\"")); + } + { + std::string test_string = "''"; + EXPECT_THAT(ConvertQuote(&test_string), Eq("\"\"")); + } + { + std::string test_string = "{'A':'a'}"; + EXPECT_THAT(ConvertQuote(&test_string), Eq("{\"A\":\"a\"}")); + } +} + +std::string QueryParamsToString(const QueryParams& params) { + std::stringstream ss; + + ss << "{ order_by="; + switch (params.order_by) { + case QueryParams::kOrderByPriority: + ss << "kOrderByPriority"; + break; + case QueryParams::kOrderByKey: + ss << "kOrderByKey"; + break; + case QueryParams::kOrderByValue: + ss << "kOrderByValue"; + break; + case QueryParams::kOrderByChild: + ss << "kOrderByChild(" << params.order_by_child << ")"; + break; + } + + if (!params.equal_to_value.is_null()) { + ss << ", equal_to_value=" << util::VariantToJson(params.equal_to_value); + } + if (!params.equal_to_child_key.empty()) { + ss << ", equal_to_child_key=" << params.equal_to_child_key; + } + if (!params.start_at_value.is_null()) { + ss << ", start_at_value=" << util::VariantToJson(params.start_at_value); + } + if (!params.start_at_child_key.empty()) { + ss << ", start_at_child_key=" << params.start_at_child_key; + } + if (!params.end_at_value.is_null()) { + ss << ", end_at_value=" << util::VariantToJson(params.end_at_value); + } + if (!params.end_at_child_key.empty()) { + ss << ", end_at_child_key=" << params.end_at_child_key; + } + if (params.limit_first != 0) { + ss << ", limit_first=" << params.limit_first; + } + if (params.limit_last != 0) { + ss << ", limit_last=" << params.limit_last; + } + ss << " }"; + + return ss.str(); +} + +// Validate the index created by IndexedVariant and its order +void VerifyIndex(const Variant* input_variant, + const QueryParams* input_query_params, TestList* expected) { + // IndexedVariant supports 4 types of constructor: + // IndexedVariant() - both input_variant and input_query_params are null + // IndexedVariant(Variant) - only input_query_params is null + // IndexedVariant(Variant, QueryParams) - both are NOT null + // Additionally, we test the copy constructor in all cases + // IndexedVariant(IndexedVariant) - A copy of an existing IndexedVariant + UniquePtr index_variant; + if (input_variant == nullptr && input_query_params == nullptr) { + index_variant = MakeUnique(); + } else if (input_variant != nullptr && input_query_params == nullptr) { + index_variant = MakeUnique(*input_variant); + } else if (input_variant != nullptr && input_query_params != nullptr) { + index_variant = + MakeUnique(*input_variant, *input_query_params); + } + + // assert if input_variant is null but input_query_params is not null + assert(index_variant); + IndexedVariant copied_index_variant(*index_variant); + + const IndexedVariant::Index* indexes[] = { + &index_variant->index(), + &copied_index_variant.index(), + }; + for (const auto& index : indexes) { + // Convert TestList::index() into TestList for comparison + TestList actual_list; + for (auto& it : *index) { + actual_list.push_back( + {it.first.AsString().string_value(), util::VariantToJson(it.second)}); + } + + for (auto& it : *expected) { + // Make sure Json string is formatted in the same way since we are doing + // string comparison. + ConvertQuote(&it.second); + it.second = util::VariantToJson(util::JsonToVariant(it.second.c_str())); + } + + EXPECT_THAT(actual_list, Eq(*expected)) + << "Test Variant: " << util::VariantToJson(*input_variant) << std::endl + << "Test QueryParams: " + << (input_query_params ? QueryParamsToString(*input_query_params) + : "null"); + } +} + +// Default IndexedVariant +TEST(IndexedVariant, ConstructorTestBasic) { + TestList expected_result = {}; + VerifyIndex(nullptr, nullptr, &expected_result); +} + +TEST(IndexedVariant, ConstructorTestDefaultQueryParamsNoPriority) { + { + Variant test_input = Variant::Null(); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(123); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(123.456); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(true); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + Variant test_input(false); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = "[1,2,3]"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = "{}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = {}; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = + "{" + " 'A': 1," + " 'B': 'b'," + " 'C':true" + "}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = { + {"A", "1"}, + {"B", "'b'"}, + {"C", "true"}, + }; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = + "{" + " 'A': 1," + " 'B': { '.value': 'b', '.priority': 100 }," + " 'C': true" + "}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = { + {"A", "1"}, + {"C", "true"}, + {"B", "{ '.value': 'b', '.priority': 100 }"}, + }; + VerifyIndex(&test_input, nullptr, &expected_result); + } + { + std::string variant_string = + "{" + " 'A': { '.value': 1, '.priority': 300 }," + " 'B': { '.value': 'b', '.priority': 100 }," + " 'C': { '.value': true, '.priority': 200 }" + "}"; + Variant test_input = + util::JsonToVariant(ConvertQuote(&variant_string).c_str()); + TestList expected_result = { + {"B", "{ '.value': 'b', '.priority': 100 }"}, + {"C", "{ '.value': true, '.priority': 200 }"}, + {"A", "{ '.value': 1, '.priority': 300 }"}, + }; + VerifyIndex(&test_input, nullptr, &expected_result); + } +} + +// Used to run individual test for GetOrderByVariantTest +// Need to access private function IndexedVariant::GetOrderByVariant(). +// Therefore this class is friended by IndexedVariant +class IndexedVariantGetOrderByVariantTest : public ::testing::Test { + protected: + void RunTest(const QueryParams& params, const Variant& key, + const TestList& value_result_list, const char* test_name) { + IndexedVariant indexed_variant(Variant::Null(), params); + + for (auto& test : value_result_list) { + std::string value_string = test.first; + Variant value = util::JsonToVariant(ConvertQuote(&value_string).c_str()); + bool expected_null = test.second.empty(); + std::string expected_string = test.second; + Variant expected = + util::JsonToVariant(ConvertQuote(&expected_string).c_str()); + + auto* result = indexed_variant.GetOrderByVariant(key, value); + EXPECT_THAT(!result || result->is_null(), Eq(expected_null)) + << test_name << " (" << key.AsString().string_value() << ", " + << value_string << ") "; + if (!expected_null && result != nullptr) { + EXPECT_THAT(*result, Eq(expected)) + << test_name << " (" << key.AsString().string_value() << ", " + << value_string << ") "; + } + } + } +}; + +TEST_F(IndexedVariantGetOrderByVariantTest, GetOrderByVariantTest) { + // Test order by priority + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", ""}, + {"{'.value': 1, '.priority': 100}", "100"}, + {"{'B': 1,'.priority': 100}", "100"}, + {"{'B': {'.value': 1, '.priority': 200} ,'.priority': 100}", "100"}, + {"{'B': {'C': 1, '.priority': 200} ,'.priority': 100}", "100"}, + }; + + RunTest(params, key, value_result_list, "OrderByPriority"); + } + + // Test order by key + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", "'A'"}, + {"{'.value': 1, '.priority': 100}", "'A'"}, + {"{'B': 1,'.priority': 100}", "'A'"}, + {"{'B': {'.value': 1, '.priority': 200} ,'.priority': 100}", "'A'"}, + {"{'B': {'C': 1, '.priority': 200} ,'.priority': 100}", "'A'"}, + }; + + RunTest(params, key, value_result_list, "OrderByKey"); + } + + // Test order by value + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + IndexedVariant indexed_variant(Variant::Null(), params); + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", "1"}, + {"{'.value': 1, '.priority': 100}", "1"}, + }; + + RunTest(params, key, value_result_list, "OrderByValue"); + } + + // Test order by child + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "B"; + IndexedVariant indexed_variant(Variant::Null(), params); + const Variant key("A"); + // List of test: { value, expected} + TestList value_result_list = { + {"1", ""}, + {"{'.value': 1, '.priority': 100}", ""}, + {"{'B': 1,'.priority': 100}", "1"}, + {"{'B': {'.value': 1, '.priority': 200} ,'.priority': 100}", "1"}, + }; + + RunTest(params, key, value_result_list, "OrderByChild"); + } +} + +TEST(IndexedVariant, FindTest) { + std::string test_data = + "{" + " 'A': 1," + " 'B': 'b'," + " 'C': true" + "}"; + Variant variant = util::JsonToVariant(ConvertQuote(&test_data).c_str()); + + IndexedVariant indexed_variant(variant); + + // List of test: { key, expected} + TestList test_list = { + {"A", "A"}, + {"B", "B"}, + {"C", "C"}, + {"D", ""}, + }; + + for (auto& test : test_list) { + auto it = indexed_variant.Find(Variant(test.first)); + + bool expected_found = !test.second.empty(); + EXPECT_THAT(it != indexed_variant.index().end(), Eq(expected_found)) + << "Find(" << test.first << ")"; + + if (expected_found && it != indexed_variant.index().end()) { + EXPECT_THAT(it->first, Eq(Variant(test.second))) + << "Find(" << test.first << ")"; + } + } +} + +TEST(IndexedVariant, GetPredecessorChildNameTest) { + std::string test_data = + "{" + " 'A': { '.value': 1, '.priority': 300 }," + " 'B': { '.value': 'b', '.priority': 100 }," + " 'C': { '.value': true, '.priority': 200 }," + " 'D': { 'E': {'.value': 'e', '.priority': 200}, '.priority': 100 }" + "}"; + + // Expected Order (Order by priority by default) + // ["B", { ".value": "b", ".priority": 100 } ], + // ["D", { "E" : {".value": "e", ".priority": 200 }, ".priority": 100 } ], + // ["C", { ".value": true, ".priority": 200 } ], + // ["A", { ".value": 1, ".priority": 300 } ] + Variant variant = util::JsonToVariant(ConvertQuote(&test_data).c_str()); + + // Use default QueryParams which uses OrderByPriority + IndexedVariant indexed_variant(variant); + + struct TestCase { + // Input key string + std::string key; + + // Input value variant, structured in Json string, with \" replaced for + // readibility. + std::string value; + + // Expected return value from GetPredecessorChildName(). If it is empty + // string (""), then the expected return value is nullptr + std::string expected_result; + }; + + std::vector test_list = { + {"A", "{ '.value': 1, '.priority': 300 }", "C"}, + // The first element, no predecessor + {"B", "{ '.value': 'b', '.priority': 100 }", ""}, + {"C", "{ '.value': true, '.priority': 200 }", "D"}, + {"D", "{ 'E': {'.value': 'e', '.priority': 200}, '.priority': 100 }", + "B"}, + // Pair not found + {"E", "'e'", ""}, + // EXCEPTION: Not found due to missing priority. + {"A", "1", ""}, + {"B", "'b'", ""}, + {"C", "true", ""}, + {"D", "{ 'E': {'.value': 'e', '.priority': 200}}", ""}, + {"D", "{ 'E': 'e'}}", ""}, + // EXCEPTION: Not found because priority is different + {"A", "{ '.value': 1, '.priority': 1000 }", ""}, + // EXCEPTION: Found because, even though the value is different, the + // priority is the same. + {"A", "{ '.value': 'a', '.priority': 300 }", "C"}, + }; + + for (auto& test : test_list) { + std::string& key = test.key; + Variant value = util::JsonToVariant(ConvertQuote(&test.value).c_str()); + const char* child_name = + indexed_variant.GetPredecessorChildName(key, value); + + std::string& expected = test.expected_result; + bool expected_found = !expected.empty(); + EXPECT_THAT(child_name != nullptr, Eq(expected_found)) + << "GetPredecessorChildNameTest(" << key << ", " << test.value << ")"; + + if (expected_found && child_name != nullptr) { + EXPECT_THAT(std::string(child_name), Eq(expected)) + << "GetPredecessorChildNameTest(" << key << ", " << test.value << ")"; + } + } +} + +TEST(IndexedVariant, Variant) { + Variant variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + QueryParams params; + IndexedVariant indexed_variant(variant, params); + Variant expected = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + EXPECT_EQ(indexed_variant.variant(), expected); +} + +TEST(IndexedVariant, UpdateChildTest) { + Variant variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + + IndexedVariant indexed_variant(variant); + + // Add new element. + IndexedVariant result1 = indexed_variant.UpdateChild("eee", 500); + // Change existing element. + IndexedVariant result2 = indexed_variant.UpdateChild("ccc", 600); + // Remove existing element. + IndexedVariant result3 = indexed_variant.UpdateChild("bbb", Variant::Null()); + + Variant expected1 = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + Variant expected2 = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 600), + std::make_pair("ddd", 400), + }; + Variant expected3 = std::map{ + std::make_pair("aaa", 100), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + EXPECT_EQ(result1.variant(), expected1); + EXPECT_EQ(result2.variant(), expected2); + EXPECT_EQ(result3.variant(), expected3); +} + +TEST(IndexedVariant, UpdatePriorityTest) { + Variant variant = 100; + IndexedVariant indexed_variant(variant); + + IndexedVariant result = indexed_variant.UpdatePriority(1234); + Variant expected = std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1234), + }; + + EXPECT_EQ(result.variant(), expected); +} + +TEST(IndexedVariant, GetFirstAndLastChildByPriority) { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + Variant variant = std::map{ + std::make_pair("aaa", + std::map{std::make_pair(".priority", 3), + std::make_pair(".value", 100)}), + std::make_pair("bbb", + std::map{std::make_pair(".priority", 4), + std::make_pair(".value", 200)}), + std::make_pair("ccc", + std::map{std::make_pair(".priority", 1), + std::make_pair(".value", 300)}), + std::make_pair("ddd", + std::map{std::make_pair(".priority", 2), + std::make_pair(".value", 400)}), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 1), + std::make_pair(".value", 300)})); + Optional> expected_last(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 4), + std::make_pair(".value", 200)})); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildByChild) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "zzz"; + Variant variant = std::map{ + std::make_pair("aaa", + std::map{std::make_pair("zzz", 2)}), + std::make_pair("bbb", + std::map{std::make_pair("zzz", 1)}), + std::make_pair("ccc", + std::map{std::make_pair("zzz", 4)}), + std::make_pair("ddd", + std::map{std::make_pair("zzz", 3)}), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first(std::make_pair( + "bbb", std::map{std::make_pair("zzz", 1)})); + Optional> expected_last(std::make_pair( + "ccc", std::map{std::make_pair("zzz", 4)})); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildByKey) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + Variant variant = std::map{ + std::make_pair("aaa", 400), + std::make_pair("bbb", 300), + std::make_pair("ccc", 200), + std::make_pair("ddd", 100), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first( + std::make_pair("aaa", 400)); + Optional> expected_last( + std::make_pair("ddd", 100)); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildByValue) { + // Value + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + Variant variant = std::map{ + std::make_pair("aaa", 400), + std::make_pair("bbb", 300), + std::make_pair("ccc", 200), + std::make_pair("ddd", 100), + }; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first( + std::make_pair("ddd", 100)); + Optional> expected_last( + std::make_pair("aaa", 400)); + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, GetFirstAndLastChildLeaf) { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + Variant variant = 1000; + IndexedVariant indexed_variant(variant, params); + Optional> expected_first; + Optional> expected_last; + EXPECT_EQ(indexed_variant.GetFirstChild(), expected_first); + EXPECT_EQ(indexed_variant.GetLastChild(), expected_last); +} + +TEST(IndexedVariant, EqualityOperatorSame) { + Variant variant(static_cast(3141592654)); + QueryParams params; + IndexedVariant indexed_variant(variant, params); + IndexedVariant identical_indexed_variant(variant, params); + + // Verify the == and != operators return the expected result. + // Check equality with self. + EXPECT_TRUE(indexed_variant == indexed_variant); + EXPECT_FALSE(indexed_variant != indexed_variant); + + // Check equality with identical change. + EXPECT_TRUE(indexed_variant == identical_indexed_variant); + EXPECT_FALSE(indexed_variant != identical_indexed_variant); +} + +TEST(IndexedVariant, EqualityOperatorDifferent) { + Variant variant(static_cast(3141592654)); + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + IndexedVariant indexed_variant(variant, params); + + Variant different_variant(static_cast(2718281828)); + QueryParams different_params; + different_params.order_by = QueryParams::kOrderByChild; + IndexedVariant indexed_variant_different_variant(different_variant, params); + IndexedVariant indexed_variant_different_params(variant, different_params); + IndexedVariant indexed_variant_different_both(different_variant, + different_params); + + // Verify the == and != operators return the expected result. + EXPECT_FALSE(indexed_variant == indexed_variant_different_variant); + EXPECT_TRUE(indexed_variant != indexed_variant_different_variant); + + EXPECT_FALSE(indexed_variant == indexed_variant_different_params); + EXPECT_TRUE(indexed_variant != indexed_variant_different_params); + + EXPECT_FALSE(indexed_variant == indexed_variant_different_both); + EXPECT_TRUE(indexed_variant != indexed_variant_different_both); +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/operation_test.cc b/database/tests/desktop/core/operation_test.cc new file mode 100644 index 0000000000..492f00ba9d --- /dev/null +++ b/database/tests/desktop/core/operation_test.cc @@ -0,0 +1,424 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/operation.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(OperationSource, ConstructorSource) { + OperationSource user_source(OperationSource::kSourceUser); + EXPECT_EQ(user_source.source, OperationSource::kSourceUser); + EXPECT_FALSE(user_source.query_params.has_value()); + EXPECT_FALSE(user_source.tagged); + + OperationSource server_source(OperationSource::kSourceServer); + EXPECT_EQ(server_source.source, OperationSource::kSourceServer); + EXPECT_FALSE(server_source.query_params.has_value()); + EXPECT_FALSE(server_source.tagged); +} + +TEST(OperationSource, ConstructorQueryParams) { + QueryParams params; + OperationSource source((Optional(params))); + + EXPECT_EQ(source.source, OperationSource::kSourceServer); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_FALSE(source.tagged); +} + +TEST(OperationSource, OperationSourceAllArgConstructor) { + QueryParams params; + { + OperationSource source(OperationSource::kSourceServer, + Optional(params), false); + + EXPECT_EQ(source.source, OperationSource::kSourceServer); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_FALSE(source.tagged); + } + { + OperationSource source(OperationSource::kSourceServer, + Optional(params), true); + + EXPECT_EQ(source.source, OperationSource::kSourceServer); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_TRUE(source.tagged); + } + { + OperationSource source(OperationSource::kSourceUser, + Optional(params), false); + + EXPECT_EQ(source.source, OperationSource::kSourceUser); + EXPECT_EQ(source.query_params.value(), params); + EXPECT_FALSE(source.tagged); + } +} + +TEST(OperationSourceDeathTest, BadConstructorArgs) { + QueryParams params; + EXPECT_DEATH(OperationSource(OperationSource::kSourceUser, + Optional(params), true), + ""); +} + +TEST(OperationSource, ForServerTaggedQuery) { + QueryParams params; + OperationSource expected(OperationSource::kSourceServer, + Optional(params), true); + + OperationSource actual = OperationSource::ForServerTaggedQuery(params); + + EXPECT_EQ(actual.source, expected.source); + EXPECT_EQ(actual.query_params, expected.query_params); + EXPECT_EQ(actual.tagged, expected.tagged); +} + +TEST(Operation, Overwrite) { + Operation op = Operation::Overwrite(OperationSource::kServer, Path("A/B/C"), + Variant(100)); + EXPECT_EQ(op.type, Operation::kTypeOverwrite); + EXPECT_EQ(op.source.source, OperationSource::kSourceServer); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); + EXPECT_EQ(op.snapshot, 100); +} + +TEST(Operation, Merge) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + Operation op = + Operation::Merge(OperationSource::kServer, Path("A/B/C"), write); + + EXPECT_EQ(op.type, Operation::kTypeMerge); + EXPECT_EQ(op.source.source, OperationSource::kSourceServer); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); + EXPECT_FALSE(op.children.IsEmpty()); + EXPECT_FALSE(op.children.write_tree().IsEmpty()); + EXPECT_FALSE(op.children.write_tree().value().has_value()); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("aaa")), 1); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("bbb")), 2); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("ccc/ddd")), 3); + EXPECT_EQ(*op.children.write_tree().GetValueAt(Path("ccc/eee")), 4); + EXPECT_EQ(op.children.write_tree().GetValueAt(Path("fff")), nullptr); +} + +TEST(Operation, AckUserWrite) { + Tree affected_tree; + affected_tree.SetValueAt(Path("Z/Y/X"), true); + affected_tree.SetValueAt(Path("Z/Y/X/W"), false); + affected_tree.SetValueAt(Path("Z/Y/X/V"), true); + affected_tree.SetValueAt(Path("Z/Y/U"), false); + Operation op = + Operation::AckUserWrite(Path("A/B/C"), affected_tree, kAckRevert); + + EXPECT_EQ(op.type, Operation::kTypeAckUserWrite); + EXPECT_EQ(op.source.source, OperationSource::kSourceUser); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); + EXPECT_TRUE(*op.affected_tree.GetValueAt(Path("Z/Y/X"))); + EXPECT_FALSE(*op.affected_tree.GetValueAt(Path("Z/Y/X/W"))); + EXPECT_TRUE(*op.affected_tree.GetValueAt(Path("Z/Y/X/V"))); + EXPECT_FALSE(*op.affected_tree.GetValueAt(Path("Z/Y/U"))); + EXPECT_TRUE(op.revert); +} + +TEST(Operation, ListenComplete) { + Operation op = + Operation::ListenComplete(OperationSource::kServer, Path("A/B/C")); + EXPECT_EQ(op.type, Operation::kTypeListenComplete); + EXPECT_EQ(op.source.source, OperationSource::kSourceServer); + EXPECT_FALSE(op.source.query_params.has_value()); + EXPECT_EQ(op.path.str(), "A/B/C"); +} + +TEST(OperationDeathTest, ListenCompleteWithWrongSource) { + // ListenCompletes must come from the server, not the user. + EXPECT_DEATH(Operation::ListenComplete(OperationSource::kUser, Path("A/B/C")), + DEATHTEST_SIGABRT); +} + +TEST(Operation, OperationForChildOverwriteEmptyPath) { + std::map variant_data{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Operation op = Operation::Overwrite(OperationSource::kServer, Path(), + Variant(variant_data)); + Optional result = OperationForChild(op, "aaa"); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeOverwrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_EQ(result->snapshot, Variant(100)); +} + +TEST(Operation, OperationForChildOverwriteNonEmptyPath) { + std::map variant_data{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Operation op = Operation::Overwrite(OperationSource::kServer, Path("A/B/C"), + Variant(variant_data)); + Optional result = OperationForChild(op, "A"); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeOverwrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); + EXPECT_EQ(result->snapshot, variant_data); +} + +TEST(Operation, OperationForChildMergeEmptyPath) { + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = Operation::Merge(OperationSource::kServer, Path(), write); + + Optional result = OperationForChild(op, "zzz"); + + EXPECT_FALSE(result.has_value()); + } + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = Operation::Merge(OperationSource::kServer, Path(), write); + + Optional result = OperationForChild(op, "aaa"); + + EXPECT_TRUE(result.has_value()); + // In this case we expect to generate an Overwrite operation. + EXPECT_EQ(result->type, Operation::kTypeOverwrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + } + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = Operation::Merge(OperationSource::kServer, Path(), write); + + Optional result = OperationForChild(op, "ccc"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeMerge); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + } +} + +TEST(Operation, OperationForChildMergeNonEmptyPath) { + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = + Operation::Merge(OperationSource::kServer, Path("A/B/C"), write); + + Optional result = OperationForChild(op, "A"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeMerge); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); + const Tree& write_tree = result->children.write_tree(); + EXPECT_EQ(*write_tree.GetValueAt(Path("aaa")), 100); + EXPECT_EQ(*write_tree.GetValueAt(Path("bbb")), 200); + EXPECT_EQ(*write_tree.GetValueAt(Path("ccc/ddd")), 300); + } + { + std::map merge_data{ + std::make_pair(Path("aaa"), 100), + std::make_pair(Path("bbb"), 200), + std::make_pair(Path("ccc/ddd"), 300), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge_data); + Operation op = + Operation::Merge(OperationSource::kServer, Path("A/B/C"), write); + + Optional result = OperationForChild(op, "Z"); + + EXPECT_FALSE(result.has_value()); + } +} + +TEST(Operation, OperationForChildAckUserWriteNonEmptyPath) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = + Operation::AckUserWrite(Path("A/B/C"), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "A"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); + EXPECT_TRUE(*result->affected_tree.GetValueAt(Path("aaa"))); + EXPECT_FALSE(*result->affected_tree.GetValueAt(Path("bbb"))); + EXPECT_TRUE(*result->affected_tree.GetValueAt(Path("ccc/ddd"))); + EXPECT_FALSE(*result->affected_tree.GetValueAt(Path("ccc/eee"))); + EXPECT_TRUE(result->revert); +} + +TEST(OperationDeathTest, + OperationForChildAckUserWriteNonEmptyPathWithUnrelatedChild) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = + Operation::AckUserWrite(Path("A/B/C"), affected_tree, kAckRevert); + + // Cannot ack an unrelated path. + EXPECT_DEATH(OperationForChild(op, "Z"), DEATHTEST_SIGABRT); +} + +TEST(Operation, OperationForChildAckUserWriteEmptyPathHasValue) { + Tree affected_tree; + affected_tree.SetValueAt(Path(), true); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "aaa"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_TRUE(result->affected_tree.value().value()); + EXPECT_TRUE(result->revert); +} + +TEST(OperationDeathTest, + OperationForChildAckUserWriteEmptyPathOverlappingChildren) { + Tree affected_tree; + affected_tree.SetValueAt(Path(), false); + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + // The affected tree has a value at the root which overlaps the affected path. + EXPECT_DEATH(OperationForChild(op, "ccc"), DEATHTEST_SIGABRT); +} + +TEST(Operation, OperationForChildAckUserWriteEmptyPathDoesNotHasValue) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "ccc"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_TRUE(*result->affected_tree.GetValueAt(Path("ddd"))); + EXPECT_FALSE(*result->affected_tree.GetValueAt(Path("eee"))); + EXPECT_TRUE(result->revert); +} + +TEST(Operation, + OperationForChildAckUserWriteEmptyPathDoesNotHasValueAndNoAffectedChild) { + Tree affected_tree; + affected_tree.SetValueAt(Path("aaa"), true); + affected_tree.SetValueAt(Path("bbb"), false); + affected_tree.SetValueAt(Path("ccc/ddd"), true); + affected_tree.SetValueAt(Path("ccc/eee"), false); + Operation op = Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + Optional result = OperationForChild(op, "zzz"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeAckUserWrite); + EXPECT_EQ(result->source.source, OperationSource::kSourceUser); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); + EXPECT_TRUE(result->affected_tree.children().empty()); + EXPECT_FALSE(result->affected_tree.value().has_value()); + EXPECT_TRUE(result->revert); +} + +TEST(Operation, OperationForChildListenCompleteEmptyPath) { + Operation op = Operation::ListenComplete(OperationSource::kServer, Path()); + + Optional result = OperationForChild(op, "Z"); + + // Should be identical to op. + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeListenComplete); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), ""); +} + +TEST(Operation, OperationForChildListenCompleteNonEmptyPath) { + Operation op = + Operation::ListenComplete(OperationSource::kServer, Path("A/B/C")); + + Optional result = OperationForChild(op, "A"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->type, Operation::kTypeListenComplete); + EXPECT_EQ(result->source.source, OperationSource::kSourceServer); + EXPECT_FALSE(result->source.query_params.has_value()); + EXPECT_EQ(result->path.str(), "B/C"); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/server_values_test.cc b/database/tests/desktop/core/server_values_test.cc new file mode 100644 index 0000000000..6fa2ebb43d --- /dev/null +++ b/database/tests/desktop/core/server_values_test.cc @@ -0,0 +1,221 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/server_values.h" + +#include + +#include "database/src/include/firebase/database/common.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// We expect the result of GenerateServerValues to be pretty close to the +// current time. It might be off by a second or so, but much more than that +// might indicate an issue. +const int kEpsilonMs = 3000; + +TEST(ServerValues, ServerTimestamp) { + EXPECT_EQ(ServerTimestamp(), Variant(std::map{ + std::make_pair(".sv", "timestamp"), + })); +} + +TEST(ServerValues, GenerateServerValues) { + int64_t current_time_ms = time(nullptr) * 1000; + + Variant result = GenerateServerValues(0); + + EXPECT_TRUE(result.is_map()); + EXPECT_EQ(result.map().size(), 1); + EXPECT_NE(result.map().find("timestamp"), result.map().end()); + EXPECT_TRUE(result.map()["timestamp"].is_int64()); + EXPECT_NEAR(result.map()["timestamp"].int64_value(), current_time_ms, + kEpsilonMs); +} + +TEST(ServerValues, GenerateServerValuesWithTimeOffset) { + int64_t current_time_ms = time(nullptr) * 1000; + + Variant result = GenerateServerValues(5000); + + EXPECT_TRUE(result.is_map()); + EXPECT_EQ(result.map().size(), 1); + EXPECT_NE(result.map().find("timestamp"), result.map().end()); + EXPECT_TRUE(result.map()["timestamp"].is_int64()); + EXPECT_NEAR(result.map()["timestamp"].int64_value(), current_time_ms + 5000, + kEpsilonMs); +} + +TEST(ServerValues, ResolveDeferredValueNull) { + Variant null_variant = Variant::Null(); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(null_variant, server_values); + + EXPECT_EQ(result, Variant::Null()); +} + +TEST(ServerValues, ResolveDeferredValueInt64) { + Variant int_variant = Variant::FromInt64(12345); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(int_variant, server_values); + + EXPECT_EQ(result, Variant::FromInt64(12345)); +} + +TEST(ServerValues, ResolveDeferredValueDouble) { + Variant double_variant = Variant::FromDouble(3.14); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(double_variant, server_values); + + EXPECT_EQ(result, Variant::FromDouble(3.14)); +} + +TEST(ServerValues, ResolveDeferredValueBool) { + Variant bool_variant = Variant::FromBool(true); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(bool_variant, server_values); + + EXPECT_EQ(result, Variant::FromBool(true)); +} + +TEST(ServerValues, ResolveDeferredValueStaticString) { + Variant static_string_variant = Variant::FromStaticString("Test"); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(static_string_variant, server_values); + + EXPECT_EQ(result, Variant::FromStaticString("Test")); +} + +TEST(ServerValues, ResolveDeferredValueMutableString) { + Variant mutable_string_variant = Variant::FromMutableString("Test"); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(mutable_string_variant, server_values); + + EXPECT_EQ(result, Variant::FromMutableString("Test")); +} + +TEST(ServerValues, ResolveDeferredValueVector) { + Variant vector_variant = std::vector{1, 2, 3, 4}; + Variant server_values = GenerateServerValues(0); + Variant expected_vector_variant = vector_variant; + + Variant result = ResolveDeferredValueSnapshot(vector_variant, server_values); + + EXPECT_EQ(result, expected_vector_variant); +} + +TEST(ServerValues, ResolveDeferredValueSimpleMap) { + Variant simple_map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant server_values = GenerateServerValues(0); + Variant expected_simple_map_variant = simple_map_variant; + + Variant result = ResolveDeferredValue(simple_map_variant, server_values); + + EXPECT_EQ(result, expected_simple_map_variant); +} + +TEST(ServerValues, ResolveDeferredValueNestedMap) { + Variant nested_map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair("fff", 500), + }), + }; + Variant expected_nested_map_variant = nested_map_variant; + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(nested_map_variant, server_values); + + EXPECT_EQ(result, expected_nested_map_variant); +} + +TEST(ServerValues, ResolveDeferredValueTimestamp) { + int64_t current_time_ms = time(nullptr) * 1000; + Variant timestamp = ServerTimestamp(); + Variant server_values = GenerateServerValues(0); + + Variant result = ResolveDeferredValue(timestamp, server_values); + + EXPECT_TRUE(result.is_int64()); + EXPECT_NEAR(result.int64_value(), current_time_ms, kEpsilonMs); +} + +TEST(ServerValues, ResolveDeferredValueSnapshot) { + int64_t current_time_ms = time(nullptr) * 1000; + Variant nested_map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair("fff", ServerTimestamp()), + }), + }; + Variant server_values = GenerateServerValues(0); + + Variant result = + ResolveDeferredValueSnapshot(nested_map_variant, server_values); + + EXPECT_EQ(result.map()["aaa"].int64_value(), 100); + EXPECT_EQ(result.map()["bbb"].int64_value(), 200); + EXPECT_EQ(result.map()["ccc"].map()["ddd"].int64_value(), 300); + EXPECT_EQ(result.map()["ccc"].map()["eee"].int64_value(), 400); + EXPECT_NEAR(result.map()["ccc"].map()["fff"].int64_value(), current_time_ms, + kEpsilonMs); +} + +TEST(ServerValues, ResolveDeferredValueMerge) { + int64_t current_time_ms = time(nullptr) * 1000; + Variant merge(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc/ddd", 300), + std::make_pair("ccc/eee", ServerTimestamp()), + }); + CompoundWrite write = CompoundWrite::FromVariantMerge(merge); + Variant server_values = GenerateServerValues(0); + + CompoundWrite result = ResolveDeferredValueMerge(write, server_values); + + EXPECT_EQ(*result.write_tree().GetValueAt(Path("aaa")), 100); + EXPECT_EQ(*result.write_tree().GetValueAt(Path("bbb")), 200); + EXPECT_EQ(*result.write_tree().GetValueAt(Path("ccc/ddd")), 300); + EXPECT_NEAR(result.write_tree().GetValueAt(Path("ccc/eee"))->int64_value(), + current_time_ms, kEpsilonMs); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/sparse_snapshot_tree_test.cc b/database/tests/desktop/core/sparse_snapshot_tree_test.cc new file mode 100644 index 0000000000..4615390d35 --- /dev/null +++ b/database/tests/desktop/core/sparse_snapshot_tree_test.cc @@ -0,0 +1,116 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/sparse_snapshot_tree.h" + +#include "app/src/variant_util.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::StrictMock; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +class Visitor { + public: + virtual ~Visitor() {} + virtual void Visit(const Path& path, const Variant& variant) = 0; +}; + +class MockVisitor : public Visitor { + public: + ~MockVisitor() override {} + MOCK_METHOD(void, Visit, (const Path& path, const Variant& variant), + (override)); +}; + +TEST(SparseSnapshotTreeTest, RememberSimple) { + SparseSnapshotTree tree; + tree.Remember(Path(), 100); + MockVisitor visitor; + + EXPECT_CALL(visitor, Visit(Path(), Variant(100))); + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +TEST(SparseSnapshotTreeTest, RememberTree) { + SparseSnapshotTree tree; + tree.Remember(Path(), std::map{std::make_pair("aaa", 100)}); + tree.Remember(Path("bbb"), 200); + tree.Remember(Path("bbb/ccc"), 300); + tree.Remember(Path("eee"), 400); + MockVisitor visitor; + + EXPECT_CALL(visitor, + Visit(Path(), Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 300), + }), + std::make_pair("eee", 400), + }))); + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +TEST(SparseSnapshotTreeTest, Forget) { + SparseSnapshotTree tree; + tree.Remember(Path(), std::map{std::make_pair("aaa", 100)}); + tree.Remember(Path("bbb"), 200); + tree.Remember(Path("bbb/ccc"), 300); + tree.Remember(Path("eee"), 400); + tree.Forget(Path("aaa")); + tree.Forget(Path("bbb")); + MockVisitor visitor; + + EXPECT_CALL(visitor, Visit(Path("eee"), Variant(400))); + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +TEST(SparseSnapshotTreeTest, Clear) { + SparseSnapshotTree tree; + tree.Remember(Path(), std::map{std::make_pair("aaa", 100)}); + tree.Remember(Path("bbb"), 200); + tree.Remember(Path("bbb/ccc"), 300); + tree.Clear(); + + // Expect no calls to this visitor. + StrictMock visitor; + + tree.ForEachTree(Path(), + [&visitor](const Path& path, const Variant& variant) { + visitor.Visit(path, variant); + }); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/sync_point_test.cc b/database/tests/desktop/core/sync_point_test.cc new file mode 100644 index 0000000000..d4605cf258 --- /dev/null +++ b/database/tests/desktop/core/sync_point_test.cc @@ -0,0 +1,390 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/sync_point.h" + +#include "app/src/include/firebase/variant.h" +#include "app/src/optional.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/cache_policy.h" +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/core/write_tree.h" +#include "database/src/desktop/persistence/persistence_manager.h" +#include "database/src/desktop/persistence/persistence_manager_interface.h" +#include "database/src/include/firebase/database/common.h" +#include "database/src/include/firebase/database/listener.h" +#include "database/tests/desktop/test/matchers.h" +#include "database/tests/desktop/test/mock_cache_policy.h" +#include "database/tests/desktop/test/mock_listener.h" +#include "database/tests/desktop/test/mock_persistence_manager.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_tracked_query_manager.h" + +using ::testing::Eq; +using ::testing::NiceMock; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(SyncPoint, IsEmpty) { + SyncPoint sync_point; + EXPECT_TRUE(sync_point.IsEmpty()); +} + +class SyncPointTest : public ::testing::Test { + public: + SyncPointTest() + : logger_(), + sync_point_(), + persistence_manager_(MakeUnique(), + MakeUnique(), + MakeUnique(), &logger_) {} + + protected: + SystemLogger logger_; + SyncPoint sync_point_; + NiceMock persistence_manager_; +}; + +TEST_F(SyncPointTest, IsNotEmpty) { + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + CacheNode server_cache; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + + sync_point_.AddEventRegistration( + UniquePtr(event_registration), writes_cache_ref, + server_cache, &persistence_manager_); + + EXPECT_FALSE(sync_point_.IsEmpty()); +} + +TEST_F(SyncPointTest, ApplyOperation) { + Operation operation; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + Variant complete_server_cache; + + std::vector results = + sync_point_.ApplyOperation(operation, writes_cache_ref, + &complete_server_cache, &persistence_manager_); + + std::vector expected_results; + + EXPECT_THAT(results, Pointwise(Eq(), expected_results)); +} + +TEST_F(SyncPointTest, AddEventRegistration) { + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + CacheNode server_cache; + // Give the EventRegistrations different QueryParams so that they get placed + // in different Views. + Path path("a/b/c"); + QueryParams value_params; + value_params.end_at_value = 222; + QuerySpec value_spec(path, value_params); + QueryParams child_params; + child_params.start_at_value = 111; + QuerySpec child_spec(path, child_params); + ValueEventRegistration* value_event_registration = + new ValueEventRegistration(nullptr, nullptr, value_spec); + ChildEventRegistration* child_event_registration = + new ChildEventRegistration(nullptr, nullptr, child_spec); + + std::vector value_events = sync_point_.AddEventRegistration( + UniquePtr(value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + std::vector child_events = sync_point_.AddEventRegistration( + UniquePtr(child_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + + std::vector view_results = sync_point_.GetIncompleteQueryViews(); + + std::vector expected_value_events; + std::vector expected_child_events; + + EXPECT_THAT(value_events, Pointwise(Eq(), expected_value_events)); + EXPECT_THAT(child_events, Pointwise(Eq(), expected_child_events)); + + // Local cache gets updated to the values it expects the server to reflect + // eventually. + + CacheNode expected_value_local_cache( + IndexedVariant(Variant::Null(), value_spec.params), false, true); + CacheNode expected_child_local_cache( + IndexedVariant(Variant::Null(), child_spec.params), false, true); + CacheNode expected_server_cache = server_cache; + + EXPECT_EQ(view_results.size(), 2); + EXPECT_EQ(view_results[0]->query_spec(), value_spec); + + EXPECT_EQ(view_results[0]->view_cache(), + ViewCache(expected_value_local_cache, expected_server_cache)); + + EXPECT_THAT(view_results[0]->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {value_event_registration})); + + EXPECT_EQ(view_results[1]->query_spec(), child_spec); + EXPECT_EQ(view_results[1]->view_cache(), + ViewCache(expected_child_local_cache, expected_server_cache)); + EXPECT_THAT(view_results[1]->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {child_event_registration})); +} + +TEST_F(SyncPointTest, RemoveEventRegistration_FromCompleteView) { + Path path("a/b/c"); + // Give the EventRegistrations different QueryParams, but neither one filters, + // so they'll get placed in the same View. + QueryParams query_params; + query_params.order_by = QueryParams::kOrderByChild; + query_params.order_by_child = "Phillip"; + QuerySpec query_spec(path, query_params); + + QueryParams another_query_params; + another_query_params.order_by = QueryParams::kOrderByChild; + another_query_params.order_by_child = "Lillian"; + QuerySpec another_query_spec(path, another_query_params); + + CacheNode server_cache(IndexedVariant(Variant(), query_spec.params), false, + false); + + MockValueListener listener; + MockValueListener another_listener; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + + ValueEventRegistration* value_event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + ValueEventRegistration* another_value_event_registration = + new ValueEventRegistration(nullptr, &another_listener, + another_query_spec); + + // Add some EventRegistrations... + sync_point_.AddEventRegistration( + UniquePtr(value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + sync_point_.AddEventRegistration( + UniquePtr(another_value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + + // ...And then remove one of them. + std::vector removed_specs; + sync_point_.RemoveEventRegistration(another_query_spec, &another_listener, + kErrorNone, &removed_specs); + + // There should be no incomplete views. + std::vector view_results = sync_point_.GetIncompleteQueryViews(); + EXPECT_EQ(view_results.size(), 0); + + // We expect that the local cache will get updated to the values that the + // server will eventually have. + CacheNode expected_local_cache( + IndexedVariant(Variant::Null(), query_spec.params), false, false); + CacheNode expected_server_cache = server_cache; + ViewCache expected_view_cache(expected_local_cache, expected_server_cache); + + // No QuerySpecs were removed, because there is only one Complete QuerySpec. + EXPECT_THAT(removed_specs, Pointwise(Eq(), std::vector{})); + + // Verify that the correct view remains. + const View* view = sync_point_.GetCompleteView(); + EXPECT_EQ(view->query_spec(), query_spec); + EXPECT_EQ(view->view_cache(), expected_view_cache); + EXPECT_THAT(view->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {value_event_registration})); +} + +TEST_F(SyncPointTest, RemoveEventRegistration_FromIncompleteView) { + Path path("a/b/c"); + // Give the EventRegistrations different QueryParams so that they get placed + // in different Views. + QueryParams query_params; + query_params.end_at_value = 222; + QuerySpec query_spec(path, query_params); + + QueryParams another_query_params; + another_query_params.start_at_value = 111; + QuerySpec another_query_spec(path, another_query_params); + + CacheNode server_cache(IndexedVariant(Variant(), query_params), false, false); + + MockValueListener listener; + MockValueListener another_listener; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + + ValueEventRegistration* value_event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + ValueEventRegistration* another_value_event_registration = + new ValueEventRegistration(nullptr, &another_listener, + another_query_spec); + + // Add some EventRegistrations... + sync_point_.AddEventRegistration( + UniquePtr(value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + sync_point_.AddEventRegistration( + UniquePtr(another_value_event_registration), + writes_cache_ref, server_cache, &persistence_manager_); + + // ...And then remove one of them. + std::vector removed_specs; + sync_point_.RemoveEventRegistration(another_query_spec, &another_listener, + kErrorNone, &removed_specs); + + // There should be one incomplete view remaining. + std::vector view_results = sync_point_.GetIncompleteQueryViews(); + EXPECT_EQ(view_results.size(), 1); + + // We expect that the local cache will get updated to the values that the + // server will eventually have. + CacheNode expected_local_cache(IndexedVariant(Variant::Null(), query_params), + false, true); + CacheNode expected_server_cache = server_cache; + ViewCache expected_view_cache(expected_local_cache, expected_server_cache); + + // Check that the correct QuerySpecs were removed. + EXPECT_THAT(removed_specs, Pointwise(Eq(), {another_query_spec})); + + // Verify that the correct view remain. + const View* view = view_results[0]; + EXPECT_EQ(view->query_spec(), query_spec); + EXPECT_EQ(view->view_cache(), expected_view_cache); + EXPECT_THAT(view->event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {value_event_registration})); +} + +TEST_F(SyncPointTest, GetCompleteServerCache) { + Path path; + + EXPECT_EQ(sync_point_.GetCompleteServerCache(path), nullptr); + EXPECT_FALSE(sync_point_.HasCompleteView()); + + // No filtering. + QueryParams apples_query_params; + QuerySpec apples_query_spec(path, apples_query_params); + + // Filtering + QueryParams bananas_query_params; + bananas_query_params.start_at_value = 111; + QuerySpec bananas_query_spec(path, bananas_query_params); + + CacheNode apples_server_cache( + IndexedVariant(Variant("Apples"), apples_query_params), true, false); + CacheNode bananas_server_cache( + IndexedVariant(Variant("Bananas"), bananas_query_params), true, false); + + MockValueListener apples_listener; + MockValueListener bananas_listener; + WriteTree writes_cache; + WriteTreeRef writes_cache_ref(Path(), &writes_cache); + + ValueEventRegistration* apples_event_registration = + new ValueEventRegistration(nullptr, &apples_listener, apples_query_spec); + ValueEventRegistration* bananas_event_registration = + new ValueEventRegistration(nullptr, &bananas_listener, + bananas_query_spec); + + sync_point_.AddEventRegistration( + UniquePtr(apples_event_registration), + writes_cache_ref, apples_server_cache, &persistence_manager_); + sync_point_.AddEventRegistration( + UniquePtr(bananas_event_registration), + writes_cache_ref, bananas_server_cache, &persistence_manager_); + + QueryParams carrots_query_params; + carrots_query_params.equal_to_value = "Carrots"; + QuerySpec carrots_query_spec(path, carrots_query_params); + EXPECT_TRUE(sync_point_.ViewExistsForQuery(apples_query_spec)); + EXPECT_TRUE(sync_point_.ViewExistsForQuery(bananas_query_spec)); + EXPECT_FALSE(sync_point_.ViewExistsForQuery(carrots_query_spec)); + + const View* apples_view = sync_point_.ViewForQuery(apples_query_spec); + const View* bananas_view = sync_point_.ViewForQuery(bananas_query_spec); + const View* carrots_view = sync_point_.ViewForQuery(carrots_query_spec); + EXPECT_EQ(apples_view->view_cache().server_snap(), apples_server_cache); + EXPECT_EQ(bananas_view->view_cache().server_snap(), bananas_server_cache); + EXPECT_EQ(carrots_view, nullptr); + + EXPECT_EQ(*sync_point_.GetCompleteServerCache(path), Variant("Apples")); + EXPECT_TRUE(sync_point_.HasCompleteView()); +} + +TEST_F(SyncPointTest, GetCompleteView_FromQuerySpecThatLoadsAllData) { + WriteTree write_tree; + WriteTreeRef write_tree_ref(Path(), &write_tree); + Path path; + + // Values to feed to AddEventRegistration that will result in a "complete" + // View, i.e. a view with no filtering (ordering is okay) + QueryParams good_params; + good_params.order_by = QueryParams::kOrderByChild; + good_params.order_by_child = "Bob"; + QuerySpec good_spec(path, good_params); + CacheNode good_server_cache(IndexedVariant(Variant("good"), good_params), + true, true); + sync_point_.AddEventRegistration( + MakeUnique(nullptr, nullptr, good_spec), + write_tree_ref, good_server_cache, &persistence_manager_); + + // Values to feed to AddEventRegistration that will not result in an + // incomplete View, i.e. a view with some filters on it. This should not be + // returned when we ask for the complete view. + QueryParams bad_params; + bad_params.limit_first = 10; + QuerySpec bad_spec(path, bad_params); + CacheNode incorrect_server_cache(IndexedVariant(Variant("bad"), bad_params), + true, true); + sync_point_.AddEventRegistration( + MakeUnique(nullptr, nullptr, bad_spec), + write_tree_ref, incorrect_server_cache, &persistence_manager_); + + const View* result = sync_point_.GetCompleteView(); + EXPECT_NE(result, nullptr); + EXPECT_EQ(result->query_spec(), good_spec); + EXPECT_EQ(result->GetLocalCache(), "good"); +} + +TEST_F(SyncPointTest, GetCompleteView_FromQuerySpecThatDoesNotLoadsAllData) { + WriteTree write_tree; + WriteTreeRef write_tree_ref(Path(), &write_tree); + Path path; + + // Values to feed to AddEventRegistration that will not result in an + // incomplete View, i.e. a view with some filters on it. This should not be + // retuened when we ask for the complete view. + QueryParams bad_params; + bad_params.limit_first = 10; + QuerySpec bad_spec(path, bad_params); + CacheNode incorrect_server_cache(IndexedVariant(Variant("bad"), bad_params), + true, true); + sync_point_.AddEventRegistration( + MakeUnique(nullptr, nullptr, bad_spec), + write_tree_ref, incorrect_server_cache, &persistence_manager_); + + EXPECT_EQ(sync_point_.GetCompleteView(), nullptr); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/sync_tree_test.cc b/database/tests/desktop/core/sync_tree_test.cc new file mode 100644 index 0000000000..fad14e5a63 --- /dev/null +++ b/database/tests/desktop/core/sync_tree_test.cc @@ -0,0 +1,825 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/core/sync_tree.h" + +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/desktop/persistence/persistence_manager.h" +#include "database/src/desktop/persistence/persistence_manager_interface.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" +#include "database/src/include/firebase/database/common.h" +#include "database/tests/desktop/test/mock_cache_policy.h" +#include "database/tests/desktop/test/mock_listen_provider.h" +#include "database/tests/desktop/test/mock_listener.h" +#include "database/tests/desktop/test/mock_persistence_manager.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_tracked_query_manager.h" +#include "database/tests/desktop/test/mock_write_tree.h" + +using ::testing::NiceMock; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::Test; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(SyncTree, Constructor) { + UniquePtr write_tree; + UniquePtr persistence_manager; + UniquePtr listen_provider; + SyncTree sync_tree(std::move(write_tree), std::move(persistence_manager), + std::move(listen_provider)); + + // Just making sure this constructor doesn't crash or leak memory. No further + // tests. +} + +class SyncTreeTest : public Test { + public: + void SetUp() override { + // These mocks are very noisy, so we make them NiceMocks and explicitly call + // EXPECT_CALL when there are specific things we expect to have happen. + UniquePtr pending_write_tree_ptr(MakeUnique()); + + persistence_storage_engine_ = new NiceMock(); + UniquePtr storage_engine_ptr( + persistence_storage_engine_); + + tracked_query_manager_ = new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager_); + + cache_policy_ = new NiceMock(); + UniquePtr cache_policy_ptr(cache_policy_); + + persistence_manager_ = new NiceMock( + std::move(storage_engine_ptr), std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger_); + UniquePtr persistence_manager_ptr( + persistence_manager_); + + listen_provider_ = new NiceMock(); + UniquePtr listen_provider_ptr(listen_provider_); + + sync_tree_ = new SyncTree(std::move(pending_write_tree_ptr), + std::move(persistence_manager_ptr), + std::move(listen_provider_ptr)); + } + + void TearDown() override { delete sync_tree_; } + + protected: + // We keep a local copy of these pointers so that we can do expectation + // testing on them. The SyncTree (or the classes SyncTree owns) own these + // pointers though so we let them handle the cleanup. + MockWriteTree* pending_write_tree_; + MockPersistenceStorageEngine* persistence_storage_engine_; + MockTrackedQueryManager* tracked_query_manager_; + MockCachePolicy* cache_policy_; + SystemLogger logger_; + MockPersistenceManager* persistence_manager_; + MockListenProvider* listen_provider_; + + SyncTree* sync_tree_; +}; + +using SyncTreeDeathTest = SyncTreeTest; + +TEST_F(SyncTreeTest, AddEventRegistration) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + EXPECT_TRUE(sync_tree_->IsEmpty()); + EXPECT_CALL(*persistence_manager_, SetQueryActive(query_spec)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + EXPECT_FALSE(sync_tree_->IsEmpty()); +} + +TEST_F(SyncTreeTest, ApplyListenComplete) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + CacheNode initial_cache(IndexedVariant(Variant(), query_spec.params), true, + false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Applying a ListenComplete should tell the PersistenceManager that listening + // on the given query is complete. + EXPECT_CALL(*persistence_manager_, SetQueryComplete(query_spec)); + std::vector results = sync_tree_->ApplyListenComplete(path); + EXPECT_EQ(results, std::vector{}); +} + +TEST_F(SyncTreeTest, ApplyServerMerge) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + std::map changed_children{ + std::make_pair(Path("fruit/apple"), "green"), + std::make_pair(Path("fruit/banana"), "yellow"), + }; + + // Apply the merge and get the results. + std::vector results = + sync_tree_->ApplyServerMerge(path, changed_children); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, ApplyServerOverwrite) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + std::map changed_children{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }; + + // Apply the override and get the results. + std::vector results = + sync_tree_->ApplyServerOverwrite(path, changed_children); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, ApplyUserMerge) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + CompoundWrite unresolved_children = + CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("fruit/apple"), "green"), + std::make_pair(Path("fruit/banana"), "yellow"), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + CompoundWrite children = unresolved_children; + WriteId write_id = 100; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserMerge(path, unresolved_children, write_id)); + + // Apply the user merge and get the results. + std::vector results = sync_tree_->ApplyUserMerge( + path, unresolved_children, children, write_id, kPersist); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, ApplyUserOverwrite) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + // Apply the user merge and get the results. + std::vector results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, AckUserWrite) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + std::vector results; + std::vector expected_results; + // Apply the user merge and get the results. + results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); + + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + results = sync_tree_->AckUserWrite(write_id, kAckConfirm, kPersist, 0); + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, AckUserWriteRevert) { + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + std::vector results; + std::vector expected_results; + // Apply the user merge and get the results. + results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); + + expected_results = { + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + results = sync_tree_->AckUserWrite(write_id, kAckRevert, kPersist, 0); + EXPECT_EQ(results, expected_results); +} + +TEST_F(SyncTreeTest, RemoveAllWrites) { + // This starts off the same as the ApplyUserOverwrite test, but then + // afterward. + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the + // PersistenceManager, but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager_, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + + // Change one element in the database, and add one new one. + Variant unresolved_new_data(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }); + // Resolved/unresolved children refer to special server values, timestamp + // specifically, which we don't support right now. + Variant new_data = unresolved_new_data; + WriteId write_id = 200; + + // Verify the values get persisted locally. + EXPECT_CALL(*persistence_manager_, + SaveUserOverwrite(path, unresolved_new_data, write_id)); + + // Apply the user merge and get the results. + std::vector results = + sync_tree_->ApplyUserOverwrite(path, unresolved_new_data, new_data, + write_id, kOverwriteVisible, kPersist); + std::vector expected_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "green"), + std::make_pair("banana", "yellow"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(results, expected_results); + + // We now have a pending write to undo. Verify we get the right events. + EXPECT_CALL(*persistence_manager_, RemoveAllUserWrites()); + std::vector remove_results = sync_tree_->RemoveAllWrites(); + std::vector expected_remove_results{ + Event(kEventTypeValue, event_registration, + DataSnapshotInternal( + nullptr, + Variant(std::map{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }), + QuerySpec(path))), + }; + EXPECT_EQ(remove_results, expected_remove_results); +} + +TEST_F(SyncTreeTest, RemoveAllEventRegistrations) { + QueryParams loads_all_data; + QueryParams does_not_load_all_data; + does_not_load_all_data.limit_first = 10; + QuerySpec query_spec1(Path("aaa/bbb/ccc"), loads_all_data); + // Two QuerySpecs at same location but different parameters. + QuerySpec query_spec2(Path("aaa/bbb/ccc"), does_not_load_all_data); + // Shadowing QuerySpec at higher location . + QuerySpec query_spec3(Path("aaa"), loads_all_data); + // QuerySpec in a totally different area of the tree. + QuerySpec query_spec4(Path("ddd/eee/fff"), does_not_load_all_data); + MockValueListener listener1; + MockChildListener listener2; + MockValueListener listener3; + MockChildListener listener4; + ValueEventRegistration* event_registration1 = + new ValueEventRegistration(nullptr, &listener1, query_spec1); + ChildEventRegistration* event_registration2 = + new ChildEventRegistration(nullptr, &listener2, query_spec2); + ValueEventRegistration* event_registration3 = + new ValueEventRegistration(nullptr, &listener3, query_spec3); + ChildEventRegistration* event_registration4 = + new ChildEventRegistration(nullptr, &listener4, query_spec4); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration1)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration2)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration3)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration4)); + + std::vector results; + // This will not cause any calls to StopListening because the listener + // is listening on aaa and redirecting changes to this location internally. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec1)).Times(2); + results = sync_tree_->RemoveAllEventRegistrations(query_spec1, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // This will cause the ListenProvider to stop listening on aaa because it is + // the rootmost listener on this location. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec3)); + EXPECT_CALL(*listen_provider_, StopListening(query_spec3, Tag())); + results = sync_tree_->RemoveAllEventRegistrations(query_spec3, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // In the case of an error, no explicit call to StopListening is made. This + // is expected. However, we will stop tracking the query. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec4)); + results = + sync_tree_->RemoveAllEventRegistrations(query_spec4, kErrorExpiredToken); + + // I have to manually construct this because normally building an 'error' + // event requres that I pass in a UniquePtr. + Event expected_event; + expected_event.type = kEventTypeError; + expected_event.event_registration = event_registration4; + expected_event.snapshot = Optional(); + expected_event.error = kErrorExpiredToken; + expected_event.path = Path("ddd/eee/fff"); + EXPECT_EQ(results, std::vector{expected_event}); +} + +TEST_F(SyncTreeTest, RemoveEventRegistration) { + QueryParams loads_all_data; + QueryParams does_not_load_all_data; + does_not_load_all_data.limit_first = 10; + QuerySpec query_spec1(Path("aaa/bbb/ccc"), loads_all_data); + // Two QuerySpecs at same location but different parameters. + QuerySpec query_spec2(Path("aaa/bbb/ccc"), does_not_load_all_data); + // Shadowing QuerySpec at higher location . + QuerySpec query_spec3(Path("aaa"), loads_all_data); + // QuerySpec in a totally different area of the tree. + QuerySpec query_spec4(Path("ddd/eee/fff"), does_not_load_all_data); + MockValueListener listener1; + MockChildListener listener2; + MockValueListener listener3; + MockChildListener listener4; + MockValueListener unassigned_listener; + ValueEventRegistration* event_registration1 = + new ValueEventRegistration(nullptr, &listener1, query_spec1); + ChildEventRegistration* event_registration2 = + new ChildEventRegistration(nullptr, &listener2, query_spec2); + ValueEventRegistration* event_registration3 = + new ValueEventRegistration(nullptr, &listener3, query_spec3); + ChildEventRegistration* event_registration4 = + new ChildEventRegistration(nullptr, &listener4, query_spec4); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration1)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration2)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration3)); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration4)); + + std::vector results; + // This will not cause any calls to StopListening because the listener + // is listening on aaa and redirecting changes to this location internally. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec1)).Times(2); + results = + sync_tree_->RemoveEventRegistration(query_spec1, &listener1, kErrorNone); + EXPECT_EQ(results, std::vector{}); + results = + sync_tree_->RemoveEventRegistration(query_spec1, &listener2, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // Expect nothing to happen. + results = sync_tree_->RemoveEventRegistration( + query_spec1, &unassigned_listener, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // This will cause the ListenProvider to stop listening on aaa because it is + // the rootmost listener on this location. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec3)); + EXPECT_CALL(*listen_provider_, StopListening(query_spec3, Tag())); + results = + sync_tree_->RemoveEventRegistration(query_spec3, &listener3, kErrorNone); + EXPECT_EQ(results, std::vector{}); + + // In the case of an error, no explicit call to StopListening is made. This + // is expected. However, we will stop tracking the query. + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec4)); + results = sync_tree_->RemoveEventRegistration(query_spec4, nullptr, + kErrorExpiredToken); + + // I have to manually construct this because normally constructing an 'error' + // event requres that I pass in a UniquePtr. + Event expected_event; + expected_event.type = kEventTypeError; + expected_event.event_registration = event_registration4; + expected_event.snapshot = Optional(); + expected_event.error = kErrorExpiredToken; + expected_event.path = Path("ddd/eee/fff"); + EXPECT_EQ(results, std::vector{expected_event}); +} + +TEST_F(SyncTreeDeathTest, RemoveEventRegistration) { + QuerySpec query_spec(Path("i/am/become/death")); + MockChildListener listener; + ChildEventRegistration* event_registration = + new ChildEventRegistration(nullptr, &listener, query_spec); + sync_tree_->AddEventRegistration( + UniquePtr(event_registration)); + EXPECT_DEATH(sync_tree_->RemoveEventRegistration(query_spec, &listener, + kErrorExpiredToken), + DEATHTEST_SIGABRT); +} + +TEST(SyncTree, CalcCompleteEventCache) { + // For this test we set up our own sync tree instead of using the premade test + // harness because we need a mock write tree instead of a functional one to + // run this test. + // + // TODO(amablue): retrofit the other tests to function with a MockWriteTree by + // filling in the expected values to calls to the write tree. + SystemLogger logger; + MockWriteTree* pending_write_tree = new NiceMock(); + UniquePtr pending_write_tree_ptr(pending_write_tree); + MockPersistenceManager* persistence_manager = + new NiceMock( + MakeUnique>(), + MakeUnique>(), + MakeUnique>(), &logger); + UniquePtr persistence_manager_ptr( + persistence_manager); + SyncTree sync_tree(std::move(pending_write_tree_ptr), + std::move(persistence_manager_ptr), + MakeUnique>()); + + Path path("aaa/bbb/ccc"); + QuerySpec query_spec(path); + MockValueListener listener; + ValueEventRegistration* event_registration = + new ValueEventRegistration(nullptr, &listener, query_spec); + + // The initial cache node would normally be set up by the PersistenceManager, + // but we're mocking it so we set it up manually. + std::map initial_variant{ + std::make_pair("fruit", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }), + }; + CacheNode initial_cache(IndexedVariant(initial_variant, query_spec.params), + true, false); + EXPECT_CALL(*persistence_manager, ServerCache(query_spec)) + .WillOnce(Return(initial_cache)); + + sync_tree.AddEventRegistration( + UniquePtr(event_registration)); + + std::vector write_ids_to_exclude{1, 2, 3, 4}; + Variant expected_server_cache(std::map{ + std::make_pair("apple", "red"), + std::make_pair("currant", "black"), + }); + EXPECT_CALL(*pending_write_tree, + CalcCompleteEventCache( + Path("aaa/bbb/ccc/fruit"), Pointee(expected_server_cache), + write_ids_to_exclude, kIncludeHiddenWrites)); + sync_tree.CalcCompleteEventCache(Path("aaa/bbb/ccc/fruit"), + write_ids_to_exclude); +} + +TEST_F(SyncTreeTest, SetKeepSynchronized) { + QuerySpec query_spec1(Path("aaa/bbb/ccc")); + QuerySpec query_spec2(Path("aaa/bbb/ccc/ddd")); + + EXPECT_CALL(*persistence_manager_, SetQueryActive(query_spec1)); + sync_tree_->SetKeepSynchronized(query_spec1, true); + + EXPECT_CALL(*persistence_manager_, SetQueryActive(query_spec2)); + sync_tree_->SetKeepSynchronized(query_spec2, true); + + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec1)); + sync_tree_->SetKeepSynchronized(query_spec1, false); + + EXPECT_CALL(*persistence_manager_, SetQueryInactive(query_spec2)); + sync_tree_->SetKeepSynchronized(query_spec2, false); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/tracked_query_manager_test.cc b/database/tests/desktop/core/tracked_query_manager_test.cc new file mode 100644 index 0000000000..1723301865 --- /dev/null +++ b/database/tests/desktop/core/tracked_query_manager_test.cc @@ -0,0 +1,396 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/tracked_query_manager.h" + +#include "app/src/logger.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" + +using testing::_; +using testing::InSequence; +using testing::NiceMock; +using testing::Return; +using testing::UnorderedElementsAre; + +namespace firebase { +namespace database { +namespace internal { + +TEST(TrackedQuery, Equality) { + TrackedQuery query(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, TrackedQuery::kInactive); + TrackedQuery same(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, TrackedQuery::kInactive); + TrackedQuery different_query_id(999, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + TrackedQuery different_query_spec(123, QuerySpec(Path("some/other/path")), + 123, TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + TrackedQuery different_complete(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kComplete, + TrackedQuery::kInactive); + TrackedQuery different_active(123, QuerySpec(Path("some/path")), 123, + TrackedQuery::kIncomplete, + TrackedQuery::kActive); + + // Check for equality. + EXPECT_TRUE(query == same); + EXPECT_FALSE(query != same); + + // Check each way it can differ. + EXPECT_FALSE(query == different_query_id); + EXPECT_TRUE(query != different_query_id); + + EXPECT_FALSE(query == different_query_spec); + EXPECT_TRUE(query != different_query_spec); + + EXPECT_FALSE(query == different_complete); + EXPECT_TRUE(query != different_complete); + + EXPECT_FALSE(query == different_active); + EXPECT_TRUE(query != different_active); +} + +TEST(TrackedQueryManager, Constructor) { + MockPersistenceStorageEngine storage_engine; + SystemLogger logger; + + InSequence seq; + EXPECT_CALL(storage_engine, BeginTransaction()); + EXPECT_CALL(storage_engine, ResetPreviouslyActiveTrackedQueries(_)); + EXPECT_CALL(storage_engine, SetTransactionSuccessful()); + EXPECT_CALL(storage_engine, EndTransaction()); + EXPECT_CALL(storage_engine, LoadTrackedQueries()); + TrackedQueryManager manager(&storage_engine, &logger); +} + +class TrackedQueryManagerTest : public ::testing::Test { + void SetUp() override { + spec_incomplete_inactive_.path = Path("test/path/incomplete_inactive"); + spec_incomplete_active_.path = Path("test/path/incomplete_active"); + spec_complete_inactive_.path = Path("test/path/complete_inactive"); + spec_complete_active_.path = Path("test/path/complete_active"); + + // Populate with fake data. + ON_CALL(storage_engine_, LoadTrackedQueries()) + .WillByDefault(Return(std::vector{ + TrackedQuery(100, spec_incomplete_inactive_, 0, + TrackedQuery::kIncomplete, TrackedQuery::kInactive), + TrackedQuery(200, spec_incomplete_active_, 0, + TrackedQuery::kIncomplete, TrackedQuery::kActive), + TrackedQuery(300, spec_complete_inactive_, 0, + TrackedQuery::kComplete, TrackedQuery::kInactive), + TrackedQuery(400, spec_complete_active_, 0, TrackedQuery::kComplete, + TrackedQuery::kActive), + })); + + manager_ = new TrackedQueryManager(&storage_engine_, &logger_); + } + + void TearDown() override { delete manager_; } + + protected: + SystemLogger logger_; + NiceMock storage_engine_; + TrackedQueryManager* manager_; + + QuerySpec spec_incomplete_inactive_; + QuerySpec spec_incomplete_active_; + QuerySpec spec_complete_inactive_; + QuerySpec spec_complete_active_; +}; + +// We need the death tests to be separate from the regular tests, but we still +// want to set up the same data. +class TrackedQueryManagerDeathTest : public TrackedQueryManagerTest {}; + +TEST_F(TrackedQueryManagerTest, FindTrackedQuery_Success) { + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_FALSE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(result->query_id, 200); + EXPECT_EQ(result->query_spec, spec_incomplete_active_); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(result->query_id, 300); + EXPECT_EQ(result->query_spec, spec_complete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_active_); + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, FindTrackedQuery_Failure) { + QuerySpec bad_spec(Path("wrong/path")); + const TrackedQuery* result = manager_->FindTrackedQuery(bad_spec); + EXPECT_EQ(result, nullptr); +} + +TEST_F(TrackedQueryManagerTest, RemoveTrackedQuery) { + EXPECT_NE(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(100)); + manager_->RemoveTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(200)); + manager_->RemoveTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(300)); + manager_->RemoveTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_NE(manager_->FindTrackedQuery(spec_complete_active_), nullptr); + + EXPECT_CALL(storage_engine_, DeleteTrackedQuery(400)); + manager_->RemoveTrackedQuery(spec_complete_active_); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_incomplete_active_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_complete_inactive_), nullptr); + EXPECT_EQ(manager_->FindTrackedQuery(spec_complete_active_), nullptr); +} + +TEST_F(TrackedQueryManagerDeathTest, RemoveTrackedQuery_Failure) { + QuerySpec not_tracked(Path("a/path/not/being/tracked")); + // Can't remove a query unless you're already tracking it. + EXPECT_DEATH(manager_->RemoveTrackedQuery(not_tracked), DEATHTEST_SIGABRT); +} + +TEST_F(TrackedQueryManagerTest, SetQueryActiveFlag_NewQuery) { + QuerySpec new_spec(Path("new/active/query")); + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(new_spec, TrackedQuery::kActive); + const TrackedQuery* result = manager_->FindTrackedQuery(new_spec); + + // result->query_id should be one digit higher than the highest query_id + // loaded. + EXPECT_EQ(result->query_id, 401); + EXPECT_EQ(result->query_spec.params, new_spec.params); + EXPECT_EQ(result->query_spec.path, new_spec.path); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryActiveFlag_ExistingQueryAlreadyTrue) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(spec_complete_active_, TrackedQuery::kActive); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_complete_active_); + + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryActiveFlag_ExistingQueryWasFalse) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(spec_incomplete_inactive_, + TrackedQuery::kActive); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerDeathTest, SetQueryInactive_NewQuery) { + QuerySpec new_spec(Path("new/active/query")); + // Can't set a query inactive unless you are already tracking it. + EXPECT_DEATH(manager_->SetQueryActiveFlag(new_spec, TrackedQuery::kInactive), + DEATHTEST_SIGABRT); +} + +TEST_F(TrackedQueryManagerTest, SetQueryInactive_ExistingQuery) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryActiveFlag(spec_complete_active_, TrackedQuery::kInactive); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_complete_active_); + + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryCompleteIfExists_DoesExist) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->SetQueryCompleteIfExists(spec_incomplete_inactive_); + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueryCompleteIfExists_DoesNotExist) { + QuerySpec new_spec(Path("new/active/query")); + manager_->SetQueryCompleteIfExists(new_spec); + const TrackedQuery* result = manager_->FindTrackedQuery(new_spec); + + EXPECT_EQ(result, nullptr); +} + +TEST_F(TrackedQueryManagerTest, SetQueriesComplete_CorrectPath) { + // Only two of our four TrackedQueries will need to be updated, and thus saved + // in the database. + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)).Times(2); + manager_->SetQueriesComplete(Path("test/path")); + + // All Tracked Queries should be complete. + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(result->query_id, 200); + EXPECT_EQ(result->query_spec, spec_incomplete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(result->query_id, 300); + EXPECT_EQ(result->query_spec, spec_complete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_active_); + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, SetQueriesComplete_IncorrectPath) { + manager_->SetQueriesComplete(Path("wrong/test/path")); + + // All Tracked Queries should be unchanged. + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_FALSE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_incomplete_active_); + EXPECT_EQ(result->query_id, 200); + EXPECT_EQ(result->query_spec, spec_incomplete_active_); + EXPECT_FALSE(result->complete); + EXPECT_TRUE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_inactive_); + EXPECT_EQ(result->query_id, 300); + EXPECT_EQ(result->query_spec, spec_complete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); + + result = manager_->FindTrackedQuery(spec_complete_active_); + EXPECT_EQ(result->query_id, 400); + EXPECT_EQ(result->query_spec, spec_complete_active_); + EXPECT_TRUE(result->complete); + EXPECT_TRUE(result->active); +} + +TEST_F(TrackedQueryManagerTest, IsQueryComplete) { + EXPECT_FALSE(manager_->IsQueryComplete(spec_incomplete_inactive_)); + EXPECT_FALSE(manager_->IsQueryComplete(spec_incomplete_active_)); + EXPECT_TRUE(manager_->IsQueryComplete(spec_complete_inactive_)); + EXPECT_TRUE(manager_->IsQueryComplete(spec_complete_active_)); + + EXPECT_FALSE(manager_->IsQueryComplete(QuerySpec(Path("nonexistant")))); +} + +TEST_F(TrackedQueryManagerTest, GetKnownCompleteChildren) { + EXPECT_THAT(manager_->GetKnownCompleteChildren(Path("test/path")), + UnorderedElementsAre("complete_inactive", "complete_active")); +} + +TEST_F(TrackedQueryManagerTest, + EnsureCompleteTrackedQuery_ExistingUncompletedQuery) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + manager_->EnsureCompleteTrackedQuery(Path("test/path/incomplete_inactive")); + + const TrackedQuery* result = + manager_->FindTrackedQuery(spec_incomplete_inactive_); + EXPECT_EQ(result->query_id, 100); + EXPECT_EQ(result->query_spec, spec_incomplete_inactive_); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, EnsureCompleteTrackedQuery_NewPath) { + EXPECT_CALL(storage_engine_, SaveTrackedQuery(_)); + Path new_path("new/path"); + manager_->EnsureCompleteTrackedQuery(new_path); + + const TrackedQuery* result = manager_->FindTrackedQuery(QuerySpec(new_path)); + EXPECT_EQ(result->query_id, 401); + EXPECT_EQ(result->query_spec, QuerySpec(new_path)); + EXPECT_TRUE(result->complete); + EXPECT_FALSE(result->active); +} + +TEST_F(TrackedQueryManagerTest, HasActiveDefaultQuery) { + EXPECT_FALSE( + manager_->HasActiveDefaultQuery(Path("test/path/incomplete_inactive"))); + EXPECT_TRUE( + manager_->HasActiveDefaultQuery(Path("test/path/incomplete_active"))); + EXPECT_FALSE( + manager_->HasActiveDefaultQuery(Path("test/path/complete_inactive"))); + EXPECT_TRUE( + manager_->HasActiveDefaultQuery(Path("test/path/complete_active"))); + + EXPECT_FALSE(manager_->IsQueryComplete(QuerySpec(Path("nonexistant")))); +} + +TEST_F(TrackedQueryManagerTest, CountOfPrunableQueries) { + EXPECT_EQ(manager_->CountOfPrunableQueries(), 2); +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/tree_test.cc b/database/tests/desktop/core/tree_test.cc new file mode 100644 index 0000000000..34b3e92a20 --- /dev/null +++ b/database/tests/desktop/core/tree_test.cc @@ -0,0 +1,1009 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/tree.h" + +#include "app/memory/unique_ptr.h" +#include "app/src/optional.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +namespace { + +using ::testing::Eq; + +typedef std::pair IntPair; + +TEST(TreeTest, DefaultConstruct) { + { + Tree tree; + EXPECT_FALSE(tree.value().has_value()); + EXPECT_EQ(tree.children().size(), 0); + } + + { + Tree tree(1); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 0); + } +} + +TEST(TreeTest, CopyConstructor) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(source); + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure source is still populated. + subtree = source.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*source.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); +} + +TEST(TreeTest, CopyAssignment) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(-9999); + destination.SetValueAt(Path("zzz/yyy/xxx"), -9999); + + destination = source; + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure old values were not left behind. + Tree* bad_subtree = destination.GetChild(Path("zzz/yyy/xxx")); + EXPECT_EQ(bad_subtree, nullptr); + + // Ensure source is still populated. + subtree = source.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*source.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); +} + +TEST(TreeTest, MoveConstructor) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(std::move(source)); + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure source is empty. + EXPECT_FALSE(source.value().has_value()); // NOLINT + EXPECT_TRUE(source.children().empty()); // NOLINT +} + +TEST(TreeTest, MoveAssignment) { + Tree source(1234); + source.SetValueAt(Path("aaa/bbb/ccc"), 5678); + Tree destination(-9999); + destination.SetValueAt(Path("zzz/yyy/xxx"), -9999); + + destination = std::move(source); + + // Ensure values got copied correctly. + Tree* subtree = destination.GetChild(Path("aaa/bbb/ccc")); + EXPECT_EQ(*destination.value(), 1234); + EXPECT_EQ(*subtree->value(), 5678); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); + + // Ensure old values were not left behind. + Tree* bad_subtree = destination.GetChild(Path("zzz/yyy/xxx")); + EXPECT_EQ(bad_subtree, nullptr); + + // Ensure source is empty. + EXPECT_FALSE(source.value().has_value()); // NOLINT + EXPECT_TRUE(source.children().empty()); // NOLINT +} + +TEST(TreeTest, GetSetValue) { + { + Tree tree(1); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + + tree.set_value(2); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 2); + } +} + +TEST(TreeTest, GetSetRValue) { + { + Tree> tree(MakeUnique(1)); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(*tree.value().value(), 1); + + tree.set_value(MakeUnique(2)); + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(*tree.value().value(), 2); + } +} + +TEST(TreeTest, GetValueAt) { + { + Tree tree; + + int* root = tree.GetValueAt(Path("")); + EXPECT_EQ(root, nullptr); + EXPECT_EQ(tree.GetValueAt(Path("A")), nullptr); + } + + { + Tree tree(1); + + int* root = tree.GetValueAt(Path("")); + EXPECT_NE(root, nullptr); + EXPECT_EQ(*root, 1); + EXPECT_EQ(tree.GetValueAt(Path("A")), nullptr); + } + + { + Tree tree(1); + tree.children()["A"].set_value(2); + tree.children()["B"].set_value(3); + + int* root = tree.GetValueAt(Path("")); + EXPECT_NE(root, nullptr); + EXPECT_EQ(*root, 1); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_NE(child_b, nullptr); + EXPECT_EQ(*child_b, 3); + } + + { + Tree tree(1); + tree.children()["A"].set_value(2); + tree.children()["A"].children()["A1"].set_value(20); + tree.children()["B"].children()["B1"].set_value(30); + + int* root = tree.GetValueAt(Path("")); + EXPECT_NE(root, nullptr); + EXPECT_EQ(*root, 1); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_a_a1 = tree.GetValueAt(Path("A/A1")); + EXPECT_NE(child_a_a1, nullptr); + EXPECT_EQ(*child_a_a1, 20); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_EQ(child_b, nullptr); + + int* child_b_b1 = tree.GetValueAt(Path("B/B1")); + EXPECT_NE(child_b_b1, nullptr); + EXPECT_EQ(*child_b_b1, 30); + } +} + +TEST(TreeTest, SetValueAt) { + { + Tree tree; + tree.SetValueAt(Path(""), 1); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 0); + } + + { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("B"), 3); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_NE(child_b, nullptr); + EXPECT_EQ(*child_b, 3); + } + + { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("A/A1"), 20); + tree.SetValueAt(Path("B/B1"), 30); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + int* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(*child_a, 2); + + int* child_a_a1 = tree.GetValueAt(Path("A/A1")); + EXPECT_NE(child_a_a1, nullptr); + EXPECT_EQ(*child_a_a1, 20); + + int* child_b = tree.GetValueAt(Path("B")); + EXPECT_EQ(child_b, nullptr); + + int* child_b_b1 = tree.GetValueAt(Path("B/B1")); + EXPECT_NE(child_b_b1, nullptr); + EXPECT_EQ(*child_b_b1, 30); + } +} + +TEST(TreeTest, SetValueAtRValue) { + { + Tree> tree; + tree.SetValueAt(Path(""), MakeUnique(1)); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 0); + } + + { + Tree> tree(MakeUnique(1)); + tree.SetValueAt(Path("A"), MakeUnique(2)); + tree.SetValueAt(Path("B"), MakeUnique(3)); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + UniquePtr* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(**child_a, 2); + + UniquePtr* child_b = tree.GetValueAt(Path("B")); + EXPECT_NE(child_b, nullptr); + EXPECT_EQ(**child_b, 3); + } + + { + Tree> tree(MakeUnique(1)); + tree.SetValueAt(Path("A"), MakeUnique(2)); + tree.SetValueAt(Path("A/A1"), MakeUnique(20)); + tree.SetValueAt(Path("B/B1"), MakeUnique(30)); + + EXPECT_TRUE(tree.value().has_value()); + EXPECT_EQ(tree.value().value(), 1); + EXPECT_EQ(tree.children().size(), 2); + + UniquePtr* child_a = tree.GetValueAt(Path("A")); + EXPECT_NE(child_a, nullptr); + EXPECT_EQ(**child_a, 2); + + UniquePtr* child_a_a1 = tree.GetValueAt(Path("A/A1")); + EXPECT_NE(child_a_a1, nullptr); + EXPECT_EQ(**child_a_a1, 20); + + UniquePtr* child_b = tree.GetValueAt(Path("B")); + EXPECT_EQ(child_b, nullptr); + + UniquePtr* child_b_b1 = tree.GetValueAt(Path("B/B1")); + EXPECT_NE(child_b_b1, nullptr); + EXPECT_EQ(**child_b_b1, 30); + } +} + +TEST(TreeTest, RootMostValue) { + { + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {3, 4}); + tree.SetValueAt(Path("A/B"), {5, 6}); + tree.SetValueAt(Path("A/B/C"), {7, 8}); + tree.SetValueAt(Path("A/B/D"), {9, 10}); + tree.SetValueAt(Path("A/B/D"), {1, 9999}); + EXPECT_EQ(*tree.RootMostValue(Path()), std::make_pair(1, 2)); + EXPECT_EQ(*tree.RootMostValue(Path("A")), std::make_pair(1, 2)); + EXPECT_EQ(*tree.RootMostValue(Path("B")), std::make_pair(1, 2)); + } + { + Tree tree; + tree.SetValueAt(Path("A/B"), {5, 6}); + tree.SetValueAt(Path("Z/Z"), {5, -9999}); + tree.SetValueAt(Path("A/B/C"), {7, 8}); + tree.SetValueAt(Path("A/B/D"), {9, 10}); + EXPECT_EQ(tree.RootMostValue(Path()), nullptr); + EXPECT_EQ(tree.RootMostValue(Path("A")), nullptr); + EXPECT_EQ(tree.RootMostValue(Path("B")), nullptr); + EXPECT_EQ(*tree.RootMostValue(Path("A/B")), std::make_pair(5, 6)); + EXPECT_EQ(*tree.RootMostValue(Path("A/B/C")), std::make_pair(5, 6)); + } + { + Tree tree; + EXPECT_EQ(tree.RootMostValue(Path()), nullptr); + } +} + +TEST(TreeTest, RootMostValueMatching) { + auto find_three = [](const IntPair& value) { return value.first == 3; }; + { + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {3, 4}); + tree.SetValueAt(Path("A/B"), {5, 6}); + tree.SetValueAt(Path("A/B/C"), {3, -9999}); + tree.SetValueAt(Path("A/B/D"), {9, 10}); + EXPECT_EQ(tree.RootMostValueMatching(Path(), find_three), nullptr); + EXPECT_EQ(*tree.RootMostValueMatching(Path("A"), find_three), + std::make_pair(3, 4)); + EXPECT_EQ(*tree.RootMostValueMatching(Path("A/B/C"), find_three), + std::make_pair(3, 4)); + EXPECT_EQ(tree.RootMostValueMatching(Path("B"), find_three), nullptr); + } + { + Tree tree; + EXPECT_EQ(tree.RootMostValueMatching(Path(), find_three), nullptr); + } +} + +TEST(TreeTest, LeafMostValue) { + { + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {1, 3}); + tree.SetValueAt(Path("A/B"), {1, 4}); + tree.SetValueAt(Path("A/B/C"), {1, 5}); + tree.SetValueAt(Path("A/B/D"), {1, 6}); + EXPECT_EQ(*tree.LeafMostValue(Path()), std::make_pair(1, 2)); + EXPECT_EQ(*tree.LeafMostValue(Path("A")), std::make_pair(1, 3)); + EXPECT_EQ(*tree.LeafMostValue(Path("A/B")), std::make_pair(1, 4)); + EXPECT_EQ(*tree.LeafMostValue(Path("A/B/C")), std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValue(Path("A/B/C/D")), std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValue(Path("B")), std::make_pair(1, 2)); + } + { + Tree tree; + EXPECT_EQ(tree.LeafMostValue(Path()), nullptr); + } +} + +TEST(TreeTest, LeafMostValueMatching) { + { + auto find_one = [](const IntPair& value) { return value.first == 1; }; + Tree tree({1, 2}); + tree.SetValueAt(Path("A"), {1, 3}); + tree.SetValueAt(Path("A/B"), {1, 4}); + tree.SetValueAt(Path("A/B/C"), {1, 5}); + tree.SetValueAt(Path("A/B/D"), {1, 6}); + EXPECT_EQ(*tree.LeafMostValueMatching(Path(), find_one), + std::make_pair(1, 2)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A"), find_one), + std::make_pair(1, 3)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A/B"), find_one), + std::make_pair(1, 4)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A/B/C"), find_one), + std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("A/B/C/D"), find_one), + std::make_pair(1, 5)); + EXPECT_EQ(*tree.LeafMostValueMatching(Path("B"), find_one), + std::make_pair(1, 2)); + } + { + Tree tree; + EXPECT_EQ(tree.LeafMostValue(Path()), nullptr); + } +} + +TEST(TreeTest, ContainsMatchingValue) { + { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("A/B"), 3); + tree.SetValueAt(Path("A/B/C"), 4); + tree.SetValueAt(Path("A/B/D"), 5); + + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 1; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 2; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 3; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 4; })); + EXPECT_TRUE( + tree.ContainsMatchingValue([](int value) { return value == 5; })); + EXPECT_FALSE( + tree.ContainsMatchingValue([](int value) { return value == 6; })); + } + { + Tree tree; + EXPECT_FALSE( + tree.ContainsMatchingValue([](int value) { return value == 0; })); + } +} + +TEST(TreeTest, GetChild) { + Tree tree(1); + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("B/B1"), 30); + + Tree* root_string = tree.GetChild(""); + const Tree* root_const_string = tree.GetChild(""); + Tree* root_path = tree.GetChild(Path("")); + const Tree* root_const_path = tree.GetChild(Path("")); + EXPECT_EQ(root_string, &tree); + EXPECT_EQ(root_const_string, &tree); + EXPECT_EQ(root_path, &tree); + EXPECT_EQ(root_const_path, &tree); + + // Test A + Tree* expected_child_a = &tree.children()["A"]; + Tree* child_a_string = tree.GetChild("A"); + const Tree* child_a_const_string = tree.GetChild("A"); + Tree* child_a_path = tree.GetChild(Path("A")); + const Tree* child_a_const_path = tree.GetChild(Path("A")); + EXPECT_EQ(child_a_string, expected_child_a); + EXPECT_EQ(child_a_const_string, expected_child_a); + EXPECT_EQ(child_a_path, expected_child_a); + EXPECT_EQ(child_a_const_path, expected_child_a); + + // Test B + Tree* expected_child_b = &tree.children()["B"]; + Tree* child_b_string = tree.GetChild("B"); + const Tree* child_b_const_string = tree.GetChild("B"); + Tree* child_b_path = tree.GetChild(Path("B")); + const Tree* child_b_const_path = tree.GetChild(Path("B")); + EXPECT_EQ(child_b_string, expected_child_b); + EXPECT_EQ(child_b_const_string, expected_child_b); + EXPECT_EQ(child_b_path, expected_child_b); + EXPECT_EQ(child_b_const_path, expected_child_b); + + // Test B/B1 + Tree* expected_child_b_b1 = &tree.children()["B"].children()["B1"]; + Tree* child_b_b1_string = + child_b_string ? child_b_string->GetChild("B1") : nullptr; + const Tree* child_b_b1_const_string = + child_b_const_string ? child_b_const_string->GetChild("B1") : nullptr; + Tree* child_b_b1_path = tree.GetChild(Path("B/B1")); + const Tree* child_b_b1_const_path = tree.GetChild(Path("B/B1")); + EXPECT_EQ(child_b_b1_string, expected_child_b_b1); + EXPECT_EQ(child_b_b1_const_string, expected_child_b_b1); + EXPECT_EQ(child_b_b1_path, expected_child_b_b1); + EXPECT_EQ(child_b_b1_const_path, expected_child_b_b1); + EXPECT_EQ(tree.GetChild("B/B1"), nullptr); + + // Test A/A1 (Does not exist) + Tree* child_a_a1_string = + child_a_string ? child_a_string->GetChild("A1") : nullptr; + const Tree* child_a_a1_const_string = + child_a_const_string ? child_a_const_string->GetChild("A1") : nullptr; + Tree* child_a_a1_path = tree.GetChild(Path("A/A1")); + const Tree* child_a_a1_const_path = tree.GetChild(Path("A/A1")); + EXPECT_EQ(child_a_a1_string, nullptr); + EXPECT_EQ(child_a_a1_const_string, nullptr); + EXPECT_EQ(child_a_a1_path, nullptr); + EXPECT_EQ(child_a_a1_const_path, nullptr); + + // Test C (Does not exist) + Tree* child_c_string = tree.GetChild("C"); + const Tree* child_c_const_string = tree.GetChild("C"); + Tree* child_c_path = tree.GetChild(Path("C")); + const Tree* child_c_const_path = tree.GetChild(Path("C")); + EXPECT_EQ(child_c_string, nullptr); + EXPECT_EQ(child_c_const_string, nullptr); + EXPECT_EQ(child_c_path, nullptr); + EXPECT_EQ(child_c_const_path, nullptr); +} + +TEST(TreeTest, IsEmpty) { + { + Tree tree; + EXPECT_TRUE(tree.IsEmpty()); + } + + { + Tree tree(1); + EXPECT_FALSE(tree.IsEmpty()); + } + + { + Tree tree; + tree.SetValueAt(Path("A"), 2); + tree.SetValueAt(Path("A/A1"), 20); + tree.SetValueAt(Path("B/B1"), 30); + EXPECT_FALSE(tree.IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("A"))->IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("A/A1"))->IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("B"))->IsEmpty()); + EXPECT_FALSE(tree.GetChild(Path("B/B1"))->IsEmpty()); + } +} + +TEST(TreeTest, GetOrMakeSubtree) { + Tree tree; + Tree* subtree; + tree.SetValueAt(Path("aaa/bbb/ccc"), 100); + + // Get existing subtree. + subtree = tree.GetOrMakeSubtree(Path("aaa/bbb/ccc")); + EXPECT_EQ(subtree->value().value(), 100); + + // Make new subtree. + subtree = tree.GetOrMakeSubtree(Path("zzz/yyy/xxx")); + EXPECT_NE(subtree, nullptr); + EXPECT_FALSE(subtree->value().has_value()); + // Now set the value, and verify the pointer we're holding updated + // appropriately. + tree.SetValueAt(Path("zzz/yyy/xxx"), 200); + EXPECT_TRUE(subtree->value().has_value()); + EXPECT_EQ(subtree->value().value(), 200); + + // Make new subtree along an exsiting path. + subtree = tree.GetOrMakeSubtree(Path("aaa/bbb/mmm")); + EXPECT_NE(subtree, nullptr); + EXPECT_FALSE(subtree->value().has_value()); + // Now set the value, and verify the pointer we're holding updated + // appropriately. + tree.SetValueAt(Path("aaa/bbb/mmm"), 300); + EXPECT_TRUE(subtree->value().has_value()); + EXPECT_EQ(subtree->value().value(), 300); +} + +TEST(TreeTest, GetPath) { + Tree tree; + const Tree* subtree = tree.GetOrMakeSubtree(Path("aaa/bbb/ccc")); + + EXPECT_EQ(tree.GetPath(), Path()); + EXPECT_EQ(subtree->GetPath(), Path("aaa/bbb/ccc")); +} + +// Record a list of visited node with its path and value +typedef std::vector> VisitedList; + +// Get a list of visited child node with its path and value +VisitedList GetVisitedChild(const Tree& tree, const Path& input_path) { + VisitedList visited; + + tree.CallOnEach( + input_path, + [](const Path& path, int* value, void* data) { + VisitedList* visited = static_cast(data); + visited->push_back(std::make_pair(path.str(), *value)); + }, + &visited); + return visited; +} + +// Get a list of visited child node with its path and value, using std::function +VisitedList GetVisitedChildStdFunction(const Tree& tree, + const Path& input_path) { + VisitedList visited; + + tree.CallOnEach(input_path, [&](const Path& path, const int& value) { + visited.push_back(std::make_pair(path.str(), value)); + }); + return visited; +} + +TEST(TreeTest, CallOnEach) { + { + Tree tree; + + Path input_path(""); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + + { + Tree tree(0); + + { + Path input_path(""); + VisitedList expected = { + {"", 0}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A"); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + } + + { + Tree tree(0); + tree.SetValueAt(Path("A"), 1); + + { + Path input_path(""); + VisitedList expected = { + {"", 0}, + {"A", 1}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A"); + VisitedList expected = { + {"A", 1}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + } + + { + Tree tree(0); + tree.SetValueAt(Path("A"), 1); + tree.SetValueAt(Path("A/A1"), 10); + tree.SetValueAt(Path("A/A2/A21"), 110); + tree.SetValueAt(Path("B/B1"), 20); + + { + Path input_path(""); + VisitedList expected = { + {"", 0}, {"A", 1}, {"A/A1", 10}, {"A/A2/A21", 110}, {"B/B1", 20}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A"); + VisitedList expected = { + {"A", 1}, + {"A/A1", 10}, + {"A/A2/A21", 110}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A/A1"); + VisitedList expected = { + {"A/A1", 10}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("A/A2"); + VisitedList expected = { + {"A/A2/A21", 110}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("B"); + VisitedList expected = { + {"B/B1", 20}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + Path input_path("B/B1"); + VisitedList expected = { + {"B/B1", 20}, + }; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + // Does not exist + Path input_path("B/B2"); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + { + // Does not exist + Path input_path("B/B1/B11"); + VisitedList expected = {}; + + EXPECT_EQ(GetVisitedChild(tree, input_path), expected); + EXPECT_EQ(GetVisitedChildStdFunction(tree, input_path), expected); + } + } +} + +TEST(TreeTest, CallOnEachAncestorIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{3, 2, 1}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachAncestor( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachAncestorDoNotIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{2, 1}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachAncestor( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + false); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{4}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant([&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantDoNotIncludeSelf) { + std::vector call_order; + std::vector expected_call_order{3, 4}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantChildrenFirst) { + std::vector call_order; + std::vector expected_call_order{4, 3}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true, true); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, CallOnEachDescendantChildrenLast) { + std::vector call_order; + std::vector expected_call_order{3, 4}; + + Tree tree; + tree.set_value(1); + tree.SetValueAt(Path("aaa"), 2); + tree.SetValueAt(Path("aaa/bbb"), 3); + tree.SetValueAt(Path("aaa/bbb/ccc"), 4); + Tree* subtree = tree.GetChild(Path("aaa/bbb")); + + subtree->CallOnEachDescendant( + [&call_order](Tree* current_tree) { + Optional value = current_tree->value(); + call_order.push_back(*value); + return false; + }, + true, false); + + EXPECT_EQ(call_order, expected_call_order); +} + +TEST(TreeTest, FindRootMostPathWithValueSuccess) { + Tree tree; + tree.SetValueAt(Path("1/2/3"), 100); + tree.SetValueAt(Path("1/2/3/4/5/6"), 200); + + Optional result = tree.FindRootMostPathWithValue(Path("1/2/3/4/5/6/7")); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), Path("1/2/3")); +} + +TEST(TreeTest, FindRootMostPathWithValueNoValue) { + Tree tree; + tree.SetValueAt(Path("a/b/c"), 100); + tree.SetValueAt(Path("a/b/c/d/e/f"), 200); + + Optional result = tree.FindRootMostPathWithValue(Path("1/2/3/4/5/6/7")); + EXPECT_FALSE(result.has_value()); +} + +TEST(TreeTest, FindRootMostMatchingPathSuccess) { + Tree tree; + tree.SetValueAt(Path("1"), 1); + tree.SetValueAt(Path("1/2"), 3); + tree.SetValueAt(Path("1/2/3"), 6); + tree.SetValueAt(Path("1/2/3/4"), 10); + tree.SetValueAt(Path("1/2/3/4/5"), 15); + tree.SetValueAt(Path("1/2/3/4/5/6"), 21); + + Optional result = tree.FindRootMostMatchingPath( + Path("1/2/3/4/5/6"), [](int value) { return value == 10; }); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), Path("1/2/3/4")); +} + +TEST(TreeTest, FindRootMostMatchingPathNoMatch) { + Tree tree; + tree.SetValueAt(Path("1"), 1); + tree.SetValueAt(Path("1/2"), 3); + tree.SetValueAt(Path("1/2/3"), 6); + tree.SetValueAt(Path("1/2/3/4"), 10); + tree.SetValueAt(Path("1/2/3/4/5"), 15); + tree.SetValueAt(Path("1/2/3/4/5/6"), 21); + + Optional result = tree.FindRootMostMatchingPath( + Path("1/2/3/4/5/6"), [](int value) { return value == 100; }); + EXPECT_FALSE(result.has_value()); +} + +TEST(TreeTest, Fold) { + Tree tree; + tree.SetValueAt(Path("1/1"), 'H'); + tree.SetValueAt(Path("1/2"), 'e'); + tree.SetValueAt(Path("1/3"), 'l'); + tree.SetValueAt(Path("1/4/1"), 'l'); + tree.SetValueAt(Path("1/4"), 'o'); + tree.SetValueAt(Path("1"), ','); + tree.SetValueAt(Path("2"), ' '); + tree.SetValueAt(Path("3/1/1"), 'w'); + tree.SetValueAt(Path("3/1/2"), 'o'); + tree.SetValueAt(Path("3/1"), 'r'); + tree.SetValueAt(Path("3/2"), 'l'); + tree.SetValueAt(Path("3"), 'd'); + tree.SetValueAt(Path("4"), '!'); + + std::string result = tree.Fold( + std::string(), + [](Path path, char value, std::string accum) { return accum += value; }); + + EXPECT_EQ(result, "Hello, world!"); +} + +TEST(TreeTest, Equality) { + Tree tree; + tree.SetValueAt(Path("1/1"), 'H'); + tree.SetValueAt(Path("1/2"), 'e'); + tree.SetValueAt(Path("1/3"), 'l'); + tree.SetValueAt(Path("1/4/1"), 'l'); + tree.SetValueAt(Path("1/4"), 'o'); + tree.SetValueAt(Path("1"), ','); + tree.SetValueAt(Path("2"), ' '); + tree.SetValueAt(Path("3/1/1"), 'w'); + tree.SetValueAt(Path("3/1/2"), 'o'); + tree.SetValueAt(Path("3/1"), 'r'); + tree.SetValueAt(Path("3/2"), 'l'); + tree.SetValueAt(Path("3"), 'd'); + tree.SetValueAt(Path("4"), '!'); + + Tree same_tree; + same_tree.SetValueAt(Path("1/1"), 'H'); + same_tree.SetValueAt(Path("1/2"), 'e'); + same_tree.SetValueAt(Path("1/3"), 'l'); + same_tree.SetValueAt(Path("1/4/1"), 'l'); + same_tree.SetValueAt(Path("1/4"), 'o'); + same_tree.SetValueAt(Path("1"), ','); + same_tree.SetValueAt(Path("2"), ' '); + same_tree.SetValueAt(Path("3/1/1"), 'w'); + same_tree.SetValueAt(Path("3/1/2"), 'o'); + same_tree.SetValueAt(Path("3/1"), 'r'); + same_tree.SetValueAt(Path("3/2"), 'l'); + same_tree.SetValueAt(Path("3"), 'd'); + same_tree.SetValueAt(Path("4"), '!'); + + Tree different_tree; + different_tree.SetValueAt(Path("1/1"), 'H'); + different_tree.SetValueAt(Path("1/2"), 'E'); + different_tree.SetValueAt(Path("1/3"), 'L'); + different_tree.SetValueAt(Path("1/4/1"), 'L'); + different_tree.SetValueAt(Path("1/4"), 'O'); + different_tree.SetValueAt(Path("1"), '!'); + different_tree.SetValueAt(Path("2"), ' '); + different_tree.SetValueAt(Path("3/1/1"), 'w'); + different_tree.SetValueAt(Path("3/1/2"), 'a'); + different_tree.SetValueAt(Path("3/1"), 'r'); + different_tree.SetValueAt(Path("3/2"), 'l'); + different_tree.SetValueAt(Path("3"), 'd'); + different_tree.SetValueAt(Path("4"), '?'); + + EXPECT_EQ(tree, same_tree); + EXPECT_NE(tree, different_tree); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/core/write_tree_test.cc b/database/tests/desktop/core/write_tree_test.cc new file mode 100644 index 0000000000..7aac190985 --- /dev/null +++ b/database/tests/desktop/core/write_tree_test.cc @@ -0,0 +1,792 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/core/write_tree.h" + +#include "app/src/variant_util.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/tests/desktop/test/mock_write_tree.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using testing::_; +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(WriteTree, ChildWrites) { + WriteTree write_tree; + WriteTreeRef ref = write_tree.ChildWrites(Path("test/path")); + + EXPECT_EQ(ref.path(), Path("test/path")); + EXPECT_EQ(ref.write_tree(), &write_tree); +} + +TEST(WriteTree, AddOverwrite) { + WriteTree write_tree; + WriteTreeRef ref = write_tree.ChildWrites(Path("test/path")); + + EXPECT_EQ(ref.path(), Path("test/path")); + EXPECT_EQ(ref.write_tree(), &write_tree); +} + +TEST(WriteTreeDeathTest, AddOverwrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path"), snap, 100, kOverwriteVisible); + + UserWriteRecord* record = write_tree.GetWrite(100); + EXPECT_TRUE(record->is_overwrite); + EXPECT_TRUE(record->visible); + EXPECT_EQ(record->path, Path("test/path")); + EXPECT_EQ(record->overwrite, snap); +} + +TEST(WriteTree, AddMerge) { + WriteTree write_tree; + CompoundWrite changed_children; + write_tree.AddMerge(Path("test/path"), changed_children, 100); + + UserWriteRecord* record = write_tree.GetWrite(100); + EXPECT_FALSE(record->is_overwrite); + EXPECT_TRUE(record->visible); + EXPECT_EQ(record->path, Path("test/path")); +} + +TEST(WriteTreeDeathTest, AddMerge) { + WriteTree write_tree; + CompoundWrite changed_children; + write_tree.AddMerge(Path("test/path"), changed_children, 100); + EXPECT_DEATH(write_tree.AddMerge(Path("test/path"), changed_children, 50), + DEATHTEST_SIGABRT); +} + +TEST(WriteTree, GetWrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one"), snap, 100, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two"), snap, 101, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/three"), snap, 102, + kOverwriteVisible); + + EXPECT_EQ(write_tree.GetWrite(99), nullptr); + EXPECT_NE(write_tree.GetWrite(100), nullptr); + EXPECT_EQ(write_tree.GetWrite(100)->path, Path("test/path/one")); + EXPECT_NE(write_tree.GetWrite(101), nullptr); + EXPECT_EQ(write_tree.GetWrite(101)->path, Path("test/path/two")); + EXPECT_NE(write_tree.GetWrite(102), nullptr); + EXPECT_EQ(write_tree.GetWrite(102)->path, Path("test/path/three")); + EXPECT_EQ(write_tree.GetWrite(103), nullptr); +} + +TEST(WriteTree, PurgeAllWrites) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one"), snap, 100, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two"), snap, 101, kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/three"), snap, 102, + kOverwriteVisible); + + std::vector purged_writes{ + UserWriteRecord(100, Path("test/path/one"), snap, kOverwriteVisible), + UserWriteRecord(101, Path("test/path/two"), snap, kOverwriteVisible), + UserWriteRecord(102, Path("test/path/three"), snap, kOverwriteVisible), + }; + EXPECT_THAT(write_tree.PurgeAllWrites(), Pointwise(Eq(), purged_writes)); +} + +TEST(WriteTree, RemoveWrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one/visible"), snap, 100, + kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two/invisible"), snap, 101, + kOverwriteInvisible); + write_tree.AddOverwrite(Path("test/path/three/visible"), snap, 102, + kOverwriteVisible); + + // Removing visible write returns true. + EXPECT_TRUE(write_tree.RemoveWrite(100)); + // Removing invisible write returns false. + EXPECT_FALSE(write_tree.RemoveWrite(101)); + + EXPECT_EQ(write_tree.GetWrite(100), nullptr); + EXPECT_EQ(write_tree.GetWrite(101), nullptr); + EXPECT_NE(write_tree.GetWrite(102), nullptr); +} + +TEST(WriteTreeDeathTest, RemoveWrite) { + WriteTree write_tree; + Variant snap("test_data"); + write_tree.AddOverwrite(Path("test/path/one/visible"), snap, 100, + kOverwriteVisible); + write_tree.AddOverwrite(Path("test/path/two/invisible"), snap, 101, + kOverwriteInvisible); + write_tree.AddOverwrite(Path("test/path/three/visible"), snap, 102, + kOverwriteVisible); + + // Cannot remove a write that never happened. + EXPECT_DEATH(write_tree.RemoveWrite(200), DEATHTEST_SIGABRT); +} + +TEST(WriteTree, GetCompleteWriteData) { + WriteTree write_tree; + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + std::make_pair(Path("ccc/fff"), Variant(std::map{ + std::make_pair("ggg", 5), + std::make_pair("hhh", 6), + })), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + EXPECT_FALSE(write_tree.GetCompleteWriteData(Path()).has_value()); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/aaa")), 1); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/bbb")), 2); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/ddd")), 3); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/eee")), 4); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/fff/ggg")), 5); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/fff/hhh")), 6); + EXPECT_EQ(*write_tree.GetCompleteWriteData(Path("test/ccc/fff/iii")), + Variant::Null()); + EXPECT_FALSE(write_tree.GetCompleteWriteData(Path("test/fff")).has_value()); + + EXPECT_FALSE(write_tree.ShadowingWrite(Path()).has_value()); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/aaa")), 1); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/bbb")), 2); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/ddd")), 3); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/eee")), 4); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/fff/ggg")), 5); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/fff/hhh")), 6); + EXPECT_EQ(*write_tree.ShadowingWrite(Path("test/ccc/fff/iii")), + Variant::Null()); + EXPECT_FALSE(write_tree.ShadowingWrite(Path("test/fff")).has_value()); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_ShadowingWrite) { + WriteTree write_tree; + Path tree_path("test/ccc"); + Variant complete_server_cache; + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, no_write_ids_to_exclude); + + Variant expected_result(std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_NoChildMerge) { + WriteTree write_tree; + Path tree_path("test/not_present"); + Variant complete_server_cache("server_cache"); + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, no_write_ids_to_exclude); + + Variant expected_result("server_cache"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_NoCompleteSnapshot) { + WriteTree write_tree; + Path tree_path("test/not_present"); + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, nullptr, no_write_ids_to_exclude); + + EXPECT_FALSE(result.has_value()); +} + +TEST(WriteTree, CalcCompleteEventCache_NoExcludes_ApplyCache) { + WriteTree write_tree; + Path tree_path("test"); + Variant complete_server_cache(std::map{ + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", -3), + std::make_pair("fff", 5), + }), + }); + std::vector no_write_ids_to_exclude; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, no_write_ids_to_exclude); + + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + std::make_pair("fff", 5), + }), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, + CalcCompleteEventCache_HasExcludes_NoHiddenWritesAndEmptyMerge) { + WriteTree write_tree; + Path tree_path("test/not_present"); + Variant complete_server_cache("server_cache"); + std::vector write_ids_to_exclude{95}; + const std::map& merge_data{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite merge = CompoundWrite::FromPathMerge(merge_data); + write_tree.AddMerge(Path("test"), merge, 100); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, write_ids_to_exclude); + + Variant expected_result("server_cache"); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventCache_HasExcludes_NoHiddenWritesAndMergeData) { + WriteTree write_tree; + Path tree_path("test"); + Variant complete_server_cache(std::map{ + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", -3), + std::make_pair("fff", 5), + }), + }); + std::vector write_ids_to_exclude{101, 102}; + const std::map& merge_100{std::make_pair(Path("aaa"), 1)}; + const std::map& merge_101{std::make_pair(Path("bbb"), 2)}; + const std::map& merge_102{std::make_pair(Path("ccc/ddd"), 3)}; + const std::map& merge_103{std::make_pair(Path("ccc/eee"), 4)}; + CompoundWrite write_100 = CompoundWrite::FromPathMerge(merge_100); + CompoundWrite write_101 = CompoundWrite::FromPathMerge(merge_101); + CompoundWrite write_102 = CompoundWrite::FromPathMerge(merge_102); + CompoundWrite write_103 = CompoundWrite::FromPathMerge(merge_103); + write_tree.AddMerge(Path("test"), write_100, 100); + write_tree.AddMerge(Path("test"), write_101, 101); + write_tree.AddMerge(Path("test"), write_102, 102); + write_tree.AddMerge(Path("test"), write_103, 103); + + Optional result = write_tree.CalcCompleteEventCache( + tree_path, &complete_server_cache, write_ids_to_exclude); + + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", -3), + std::make_pair("eee", 4), + std::make_pair("fff", 5), + }), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventChildren_WithTopLevelSet) { + WriteTree write_tree; + Path tree_path("test/ccc"); + Variant complete_server_children("Irrelevant"); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Variant result = + write_tree.CalcCompleteEventChildren(tree_path, complete_server_children); + Variant expected_result(std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }); + EXPECT_EQ(result, expected_result); +} + +TEST(WriteTree, CalcCompleteEventChildren_WithoutTopLevelSet) { + WriteTree write_tree; + Path tree_path("test"); + Variant complete_server_children(std::map{ + std::make_pair("zzz", -1), + std::make_pair("yyy", -2), + }); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Variant result = + write_tree.CalcCompleteEventChildren(tree_path, complete_server_children); + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + std::make_pair("zzz", -1), + std::make_pair("yyy", -2), + }); + + EXPECT_EQ(result, expected_result); +} + +TEST(WriteTree, CalcEventCacheAfterServerOverwrite_NoWritesAreShadowing) { + WriteTree write_tree; + Path tree_path("test/ccc"); + Path child_path("ddd"); + Variant existing_local_snap; + Variant exsiting_server_snap(std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + // Given that the underlying server data has updated, determine what, if + // anything, needs to be applied to the event cache. In this case, no writes + // are shadowing. Events should be raised, the snap to be applied comes from + // the server data. + Optional result = write_tree.CalcEventCacheAfterServerOverwrite( + tree_path, child_path, &existing_local_snap, &exsiting_server_snap); + Variant expected_result = 3; + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcEventCacheAfterServerOverwrite_CompleteShadowing) { + WriteTree write_tree; + Path tree_path("test"); + Path child_path("aaa"); + Variant existing_local_snap; + Variant exsiting_server_snap; + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc"), + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + // Given that the underlying server data has updated, determine what, if + // anything, needs to be applied to the event cache. The write at "test/aaa" + // is completely shadowed by what is already in the tree. + Optional result = write_tree.CalcEventCacheAfterServerOverwrite( + tree_path, child_path, &existing_local_snap, &exsiting_server_snap); + + EXPECT_FALSE(result.has_value()); +} + +TEST(WriteTree, CalcEventCacheAfterServerOverwrite_PartiallyShadowed) { + WriteTree write_tree; + Path tree_path("test"); + Path child_path; + Variant existing_local_snap; + Variant exsiting_server_snap(std::map{ + std::make_pair("zzz", 100), + }); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + // Given that the underlying server data has updated, determine what, if + // anything, needs to be applied to the event cache. The write at "test" is + // partially shadowed, so we'll need to merge the server snap with the write + // to get the updated snapshot. + Optional result = write_tree.CalcEventCacheAfterServerOverwrite( + tree_path, child_path, &existing_local_snap, &exsiting_server_snap); + Variant expected_result(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + std::make_pair("zzz", 100), + }); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTreeDeathTest, CalcEventCacheAfterServerOverwrite) { + WriteTree write_tree; + EXPECT_DEATH(write_tree.CalcEventCacheAfterServerOverwrite(Path(), Path(), + nullptr, nullptr), + DEATHTEST_SIGABRT); +} + +TEST(WriteTree, CalcCompleteChild_HasShadowingVariant) { + WriteTree write_tree; + Path tree_path("test"); + std::string child_key("aaa"); + CacheNode existing_server_cache; + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Optional result = + write_tree.CalcCompleteChild(tree_path, child_key, existing_server_cache); + Variant expected_result = 1; + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteChild_HasCompleteChild) { + WriteTree write_tree; + Path tree_path("test"); + std::string child_key("bbb"); + CacheNode existing_server_cache( + IndexedVariant(std::map{std::make_pair("bbb", 2)}), + true, false); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Optional result = + write_tree.CalcCompleteChild(tree_path, child_key, existing_server_cache); + Variant expected_result = 2; + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcCompleteChild_NoCompleteChild) { + WriteTree write_tree; + Path tree_path("test"); + std::string child_key("ccc"); + CacheNode existing_server_cache( + IndexedVariant(std::map{std::make_pair("bbb", 2)}), + true, false); + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + }; + write_tree.AddMerge(Path("test"), CompoundWrite::FromPathMerge(merge), 100); + + Variant expected_result; + Optional result = + write_tree.CalcCompleteChild(tree_path, child_key, existing_server_cache); + + EXPECT_EQ(*result, expected_result); +} + +TEST(WriteTree, CalcNextVariantAfterPost_WithShadowingVariant) { + WriteTree write_tree; + write_tree.AddOverwrite(Path("test"), + Variant(std::map{ + std::make_pair("aaa", 5), + std::make_pair("bbb", 4), + std::make_pair("ccc", 3), + std::make_pair("ddd", 2), + std::make_pair("eee", 1), + }), + 101, kOverwriteVisible); + + Path tree_path("test"); + Optional complete_server_data; + IterationDirection direction = kIterateForward; + QuerySpec query_spec; + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("aaa", 5), + direction, query_spec.params), + std::make_pair(Variant("bbb"), Variant(4))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("bbb", 4), + direction, query_spec.params), + std::make_pair(Variant("ccc"), Variant(3))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ccc", 3), + direction, query_spec.params), + std::make_pair(Variant("ddd"), Variant(2))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ddd", 2), + direction, query_spec.params), + std::make_pair(Variant("eee"), Variant(1))); + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("eee", 1), + direction, query_spec.params) + .has_value()); +} + +TEST(WriteTree, CalcNextVariantAfterPost_WithoutShadowingVariant) { + WriteTree write_tree; + Path tree_path("test"); + Optional complete_server_data(std::map{ + std::make_pair("aaa", 5), + std::make_pair("bbb", 4), + std::make_pair("ccc", 3), + std::make_pair("ddd", 2), + std::make_pair("eee", 1), + }); + IterationDirection direction = kIterateForward; + QuerySpec query_spec; + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("aaa", 5), + direction, query_spec.params), + std::make_pair(Variant("bbb"), Variant(4))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("bbb", 4), + direction, query_spec.params), + std::make_pair(Variant("ccc"), Variant(3))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ccc", 3), + direction, query_spec.params), + std::make_pair(Variant("ddd"), Variant(2))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ddd", 2), + direction, query_spec.params), + std::make_pair(Variant("eee"), Variant(1))); + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("eee", 1), + direction, query_spec.params) + .has_value()); +} + +TEST(WriteTree, CalcNextVariantAfterPost_WithoutShadowingVariantOrServerData) { + WriteTree write_tree; + Path tree_path("test"); + Optional complete_server_data; + IterationDirection direction = kIterateForward; + QuerySpec query_spec; + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("aaa", 5), + direction, query_spec.params) + .has_value()); +} + +TEST(WriteTree, CalcNextVariantAfterPost_Reverse) { + WriteTree write_tree; + write_tree.AddOverwrite(Path("test"), + Variant(std::map{ + std::make_pair("aaa", 5), + std::make_pair("bbb", 4), + std::make_pair("ccc", 3), + std::make_pair("ddd", 2), + std::make_pair("eee", 1), + }), + 101, kOverwriteVisible); + + Path tree_path("test"); + Optional complete_server_data; + IterationDirection direction = kIterateReverse; + QuerySpec query_spec; + EXPECT_FALSE(write_tree + .CalcNextVariantAfterPost(tree_path, complete_server_data, + std::make_pair("aaa", 5), + kIterateReverse, query_spec.params) + .has_value()); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("bbb", 4), + direction, query_spec.params), + std::make_pair(Variant("aaa"), Variant(5))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ccc", 3), + direction, query_spec.params), + std::make_pair(Variant("bbb"), Variant(4))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("ddd", 2), + direction, query_spec.params), + std::make_pair(Variant("ccc"), Variant(3))); + EXPECT_EQ(*write_tree.CalcNextVariantAfterPost( + tree_path, complete_server_data, std::make_pair("eee", 1), + direction, query_spec.params), + std::make_pair(Variant("ddd"), Variant(2))); +} + +TEST(WriteTreeRef, Constructor) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + + EXPECT_EQ(ref.path(), Path("test/path")); + EXPECT_EQ(ref.write_tree(), &write_tree); +} + +TEST(WriteTreeRef, CalcCompleteEventCache1) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Variant complete_server_cache; + + EXPECT_CALL(write_tree, CalcCompleteEventCache(Eq(Path("test/path")), + Eq(&complete_server_cache))); + ref.CalcCompleteEventCache(&complete_server_cache); +} + +TEST(WriteTreeRef, CalcCompleteEventCache2) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Variant complete_server_cache; + std::vector write_ids_to_exclude; + + EXPECT_CALL(write_tree, CalcCompleteEventCache(Eq(Path("test/path")), + Eq(&complete_server_cache), + Eq(write_ids_to_exclude))); + ref.CalcCompleteEventCache(&complete_server_cache, write_ids_to_exclude); +} + +TEST(WriteTreeRef, CalcCompleteEventCache3) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Variant complete_server_cache; + std::vector write_ids_to_exclude; + HiddenWriteInclusion include_hidden_writes = kExcludeHiddenWrites; + + EXPECT_CALL(write_tree, CalcCompleteEventCache(Eq(Path("test/path")), + Eq(&complete_server_cache), + Eq(write_ids_to_exclude), + Eq(include_hidden_writes))); + ref.CalcCompleteEventCache(&complete_server_cache, write_ids_to_exclude, + include_hidden_writes); +} + +TEST(WriteTreeRef, CalcEventCacheAfterServerOverwrite) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Path path("another/path"); + Variant existing_local_snap; + Variant existing_server_snap; + + EXPECT_CALL(write_tree, + CalcEventCacheAfterServerOverwrite( + Eq(Path("test/path")), Eq(Path("another/path")), + Eq(&existing_local_snap), Eq(&existing_server_snap))); + ref.CalcEventCacheAfterServerOverwrite(path, &existing_local_snap, + &existing_server_snap); +} + +TEST(WriteTreeRef, ShadowingWrite) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + Path path("another/path"); + + EXPECT_CALL(write_tree, ShadowingWrite(Eq(Path("test/path/another/path")))); + ref.ShadowingWrite(path); +} + +TEST(WriteTreeRef, CalcCompleteChild) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + CacheNode existing_server_cache; + + EXPECT_CALL(write_tree, + CalcCompleteChild(Eq(Path("test/path")), Eq("child_key"), _)); + ref.CalcCompleteChild("child_key", existing_server_cache); +} + +TEST(WriteTreeRef, Child) { + MockWriteTree write_tree; + WriteTreeRef ref(Path("test/path"), &write_tree); + + WriteTreeRef child_ref = ref.Child("child_key"); + + EXPECT_EQ(child_ref.path(), Path("test/path/child_key")); + EXPECT_EQ(child_ref.write_tree(), &write_tree); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/mutable_data_desktop_test.cc b/database/tests/desktop/mutable_data_desktop_test.cc new file mode 100644 index 0000000000..3af7c11415 --- /dev/null +++ b/database/tests/desktop/mutable_data_desktop_test.cc @@ -0,0 +1,237 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/mutable_data_desktop.h" + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; + +namespace firebase { +namespace database { +namespace internal { + +TEST(MutableDataTest, TestBasic) { + { + MutableDataInternal data(nullptr, Variant::Null()); + EXPECT_THAT(data.GetChildren().size(), Eq(0)); + EXPECT_THAT(data.GetChildrenCount(), Eq(0)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), Eq(Variant::Null())); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_FALSE(data.HasChild("A")); + } + + { + MutableDataInternal data(nullptr, Variant(10)); + EXPECT_THAT(data.GetChildren().size(), Eq(0)); + EXPECT_THAT(data.GetChildrenCount(), Eq(0)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_FALSE(data.HasChild("A")); + } + + { + MutableDataInternal data( + nullptr, util::JsonToVariant("{\".value\":10,\".priority\":1}")); + EXPECT_THAT(data.GetChildren().size(), Eq(0)); + EXPECT_THAT(data.GetChildrenCount(), Eq(0)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_FALSE(data.HasChild("A")); + } + + { + MutableDataInternal data( + nullptr, + util::JsonToVariant("{\"A\":{\"B\":{\"C\":10}},\".priority\":1}")); + EXPECT_THAT(data.GetChildren().size(), Eq(1)); + EXPECT_THAT(data.GetChildrenCount(), Eq(1)); + EXPECT_THAT(data.GetKeyString(), Eq("")); + EXPECT_THAT(data.GetValue(), + Eq(util::JsonToVariant("{\"A\":{\"B\":{\"C\":10}}}"))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_TRUE(data.HasChild("A")); + EXPECT_TRUE(data.HasChild("A/B")); + EXPECT_TRUE(data.HasChild("A/B/C")); + EXPECT_FALSE(data.HasChild("A/B/C/D")); + EXPECT_FALSE(data.HasChild("D")); + + auto child_a = data.Child("A"); + EXPECT_THAT(child_a->GetChildren().size(), Eq(1)); + EXPECT_THAT(child_a->GetChildrenCount(), Eq(1)); + EXPECT_THAT(child_a->GetKeyString(), Eq("A")); + EXPECT_THAT(child_a->GetValue(), + Eq(util::JsonToVariant("{\"B\":{\"C\":10}}"))); + EXPECT_THAT(child_a->GetPriority(), Eq(Variant::Null())); + EXPECT_TRUE(child_a->HasChild("B")); + EXPECT_TRUE(child_a->HasChild("B/C")); + EXPECT_FALSE(child_a->HasChild("B/C/D")); + EXPECT_FALSE(child_a->HasChild("D")); + + delete child_a; + } +} + +TEST(MutableDataTest, TestWrite) { + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue(Variant(10)); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), Eq(Variant(10))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetPriority(Variant(1)); + EXPECT_THAT(data.GetValue(), Eq(Variant::Null())); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), Eq(Variant::Null())); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue(Variant(10)); + data.SetPriority(Variant(1)); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\".value\":10}"))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetPriority(Variant(1)); + data.SetValue(Variant(10)); + EXPECT_THAT(data.GetValue(), Eq(Variant(10))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), Eq(util::JsonToVariant("10"))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue(util::JsonToVariant("{\"A\":10,\"B\":20}")); + data.SetPriority(Variant(1)); + EXPECT_THAT(data.GetValue(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + EXPECT_THAT(data.GetPriority(), Eq(Variant(1))); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"A\":10,\"B\":20}"))); + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + data.SetPriority(Variant(1)); + data.SetValue(util::JsonToVariant("{\"A\":10,\"B\":20}")); + EXPECT_THAT(data.GetValue(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + EXPECT_THAT(data.GetPriority(), Eq(Variant::Null())); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + } +} + +TEST(MutableDataTest, TestChild) { + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child_a = data.Child("A"); + child_a->SetValue(Variant(10)); + auto child_b = data.Child("B"); + child_b->SetValue(Variant(20)); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":10,\"B\":20}"))); + + delete child_a; + delete child_b; + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child_a = data.Child("A"); + child_a->SetValue(Variant(10)); + auto child_b = child_a->Child("B"); + child_b->SetValue(Variant(20)); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":{\"B\":20}}"))); + + delete child_a; + delete child_b; + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child = data.Child("A/B"); + child->SetValue(Variant(20)); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\"A\":{\"B\":20}}"))); + + delete child; + } + + { + MutableDataInternal data(nullptr, Variant::Null()); + auto child_1 = data.Child("A/B/C"); + child_1->SetValue(Variant(20)); + child_1->SetPriority(Variant(3)); + auto child_2 = data.Child("A"); + child_2->SetPriority(Variant(2)); + data.SetPriority(Variant(1)); + EXPECT_THAT( + data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"A\":{\".priority\":2,\"B\":{" + "\"C\":{\".priority\":3,\".value\":20}}}}"))); + + delete child_1; + delete child_2; + } + + { + // Test GetValue() to convert applicable map to vector + MutableDataInternal data(nullptr, Variant::Null()); + auto child_1 = data.Child("0"); + child_1->SetValue(0); + auto child_2 = data.Child("2"); + child_2->SetValue(2); + child_2->SetPriority(20); + data.SetPriority(1); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"0\":0,\"2\":{\"." + "value\":2,\".priority\":20}}"))); + EXPECT_THAT(data.GetValue(), Eq(util::JsonToVariant("[0,null,2]"))); + + delete child_1; + delete child_2; + } + + { + // Set value with vector and priority + MutableDataInternal data(nullptr, Variant::Null()); + data.SetValue( + util::JsonToVariant("{\".priority\":1,\".value\":[0,null,{\".value\":2," + "\".priority\":20}]}")); + EXPECT_THAT(data.GetHolder(), + Eq(util::JsonToVariant("{\".priority\":1,\"0\":0,\"2\":{\"." + "value\":2,\".priority\":20}}"))); + EXPECT_THAT(data.GetValue(), Eq(util::JsonToVariant("[0,null,2]"))); + } +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/flatbuffer_conversions_test.cc b/database/tests/desktop/persistence/flatbuffer_conversions_test.cc new file mode 100644 index 0000000000..d09ddeb94f --- /dev/null +++ b/database/tests/desktop/persistence/flatbuffer_conversions_test.cc @@ -0,0 +1,458 @@ +// Copyright 2020 Google LLC +// +// 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 "database/src/desktop/persistence/flatbuffer_conversions.h" + +#include +#include + +#include "app/src/include/firebase/variant.h" +#include "app/src/variant_util.h" +#include "app/tests/flexbuffer_matcher.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/src/desktop/persistence/persisted_compound_write_generated.h" +#include "database/src/desktop/persistence/persisted_query_params_generated.h" +#include "database/src/desktop/persistence/persisted_query_spec_generated.h" +#include "database/src/desktop/persistence/persisted_tracked_query_generated.h" +#include "database/src/desktop/persistence/persisted_user_write_record_generated.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" + +using firebase::database::internal::persistence::CreatePersistedCompoundWrite; +using firebase::database::internal::persistence::CreatePersistedQueryParams; +using firebase::database::internal::persistence::CreatePersistedQuerySpec; +using firebase::database::internal::persistence::CreatePersistedTrackedQuery; +using firebase::database::internal::persistence::CreateTreeKeyValuePair; +using firebase::database::internal::persistence::CreateVariantTreeNode; + +using firebase::database::internal::persistence:: + FinishPersistedCompoundWriteBuffer; +using firebase::database::internal::persistence:: + FinishPersistedQueryParamsBuffer; +using firebase::database::internal::persistence::FinishPersistedQuerySpecBuffer; +using firebase::database::internal::persistence:: + FinishPersistedTrackedQueryBuffer; +using firebase::database::internal::persistence:: + FinishPersistedUserWriteRecordBuffer; + +using firebase::util::VariantToFlexbuffer; +using flatbuffers::FlatBufferBuilder; +using flatbuffers::Offset; + +// This makes it easier to understand what all the 0's mean. +static const int kFlatbufferEmptyField = 0; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// This is just to make some of the tests clearer. +typedef std::vector> + VectorOfKeyValuePairs; + +TEST(FlatBufferConversion, CompoundWriteFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedCompoundWriteBuffer( + builder, + CreatePersistedCompoundWrite(builder, CreateVariantTreeNode( + builder, + kFlatbufferEmptyField, + builder.CreateVector(VectorOfKeyValuePairs{ + CreateTreeKeyValuePair( + builder, + builder.CreateString("aaa"), + CreateVariantTreeNode( + builder, + kFlatbufferEmptyField, + builder.CreateVector(VectorOfKeyValuePairs{ + CreateTreeKeyValuePair( + builder, + builder.CreateString("bbb"), + CreateVariantTreeNode( + builder, + builder.CreateVector( + VariantToFlexbuffer(100)), + kFlatbufferEmptyField)) + }) + ) + ) + }) + )) + ); + // clang-format on + + const persistence::PersistedCompoundWrite* persisted_compound_write = + persistence::GetPersistedCompoundWrite(builder.GetBufferPointer()); + CompoundWrite result = CompoundWriteFromFlatbuffer(persisted_compound_write); + + CompoundWrite expected_result; + expected_result.AddWriteInline(Path("aaa/bbb"), 100); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, QueryParamsFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedQueryParamsBuffer( + builder, + persistence::CreatePersistedQueryParams( + builder, + persistence::OrderBy_Value, + builder.CreateString("order_by_child"), + builder.CreateVector(VariantToFlexbuffer(1234)), + builder.CreateString("start_at"), + builder.CreateVector(VariantToFlexbuffer(9876)), + builder.CreateString("end_at"), + builder.CreateVector(VariantToFlexbuffer(5555)), + builder.CreateString("equal_to"), + 3333, + 6666)); + // clang-format on + + const persistence::PersistedQueryParams* persisted_query_params = + persistence::GetPersistedQueryParams(builder.GetBufferPointer()); + QueryParams result = QueryParamsFromFlatbuffer(persisted_query_params); + + QueryParams expected_result; + expected_result.order_by = QueryParams::kOrderByValue; + expected_result.order_by_child = "order_by_child"; + expected_result.start_at_value = Variant::FromInt64(1234); + expected_result.start_at_child_key = "start_at"; + expected_result.end_at_value = Variant::FromInt64(9876); + expected_result.end_at_child_key = "end_at"; + expected_result.equal_to_value = Variant::FromInt64(5555); + expected_result.equal_to_child_key = "equal_to"; + expected_result.limit_first = 3333; + expected_result.limit_last = 6666; + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, QuerySpecFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedQuerySpecBuffer( + builder, + CreatePersistedQuerySpec( + builder, + builder.CreateString("this/is/a/path/to/a/thing"), + CreatePersistedQueryParams( + builder, + persistence::OrderBy_Value, + builder.CreateString("order_by_child"), + builder.CreateVector(VariantToFlexbuffer(1234)), + builder.CreateString("start_at"), + builder.CreateVector(VariantToFlexbuffer(9876)), + builder.CreateString("end_at"), + builder.CreateVector(VariantToFlexbuffer(5555)), + builder.CreateString("equal_to"), + 3333, + 6666))); + // clang-format on + + const persistence::PersistedQuerySpec* persisted_query_spec = + persistence::GetPersistedQuerySpec(builder.GetBufferPointer()); + QuerySpec result = QuerySpecFromFlatbuffer(persisted_query_spec); + + QuerySpec expected_result; + expected_result.params.order_by = QueryParams::kOrderByValue; + expected_result.params.order_by_child = "order_by_child"; + expected_result.params.start_at_value = Variant::FromInt64(1234); + expected_result.params.start_at_child_key = "start_at"; + expected_result.params.end_at_value = Variant::FromInt64(9876); + expected_result.params.end_at_child_key = "end_at"; + expected_result.params.equal_to_value = Variant::FromInt64(5555); + expected_result.params.equal_to_child_key = "equal_to"; + expected_result.params.limit_first = 3333; + expected_result.params.limit_last = 6666; + expected_result.path = Path("this/is/a/path/to/a/thing"); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, TrackedQueryFromFlatbuffer) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedTrackedQueryBuffer( + builder, + CreatePersistedTrackedQuery( + builder, + 9999, + CreatePersistedQuerySpec( + builder, + builder.CreateString("some/path"), + CreatePersistedQueryParams( + builder, + persistence::OrderBy_Value, + builder.CreateString("order_by_child"), + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + kFlatbufferEmptyField, + 0, + 0)), + 543024000, + false, + true)); + // clang-format on + + const persistence::PersistedTrackedQuery* persisted_query_spec = + persistence::GetPersistedTrackedQuery(builder.GetBufferPointer()); + TrackedQuery result = TrackedQueryFromFlatbuffer(persisted_query_spec); + + TrackedQuery expected_result; + expected_result.query_id = 9999; + expected_result.query_spec.params.order_by = QueryParams::kOrderByValue; + expected_result.query_spec.params.order_by_child = "order_by_child"; + expected_result.query_spec.path = Path("some/path"); + expected_result.last_use = 543024000; + expected_result.complete = false; + expected_result.active = true; + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, UserWriteRecordFromFlatbuffer_Overwrite) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedUserWriteRecordBuffer( + builder, + persistence::CreatePersistedUserWriteRecord( + builder, + 1234, + builder.CreateString("this/is/a/path/to/a/thing"), + builder.CreateVector(VariantToFlexbuffer("flexbuffer")), + kFlatbufferEmptyField, + true, + true)); + // clang-format on + + const persistence::PersistedUserWriteRecord* persisted_user_write_record = + persistence::GetPersistedUserWriteRecord(builder.GetBufferPointer()); + UserWriteRecord result = + UserWriteRecordFromFlatbuffer(persisted_user_write_record); + + UserWriteRecord expected_result(1234, Path("this/is/a/path/to/a/thing"), + Variant::FromStaticString("flexbuffer"), + true); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, UserWriteRecordFromFlatbuffer_Merge) { + FlatBufferBuilder builder; + + // clang-format off + FinishPersistedUserWriteRecordBuffer( + builder, + persistence::CreatePersistedUserWriteRecord( + builder, + 1234, + builder.CreateString("this/is/a/path/to/a/thing"), + kFlatbufferEmptyField, + CreatePersistedCompoundWrite( + builder, + CreateVariantTreeNode( + builder, + kFlatbufferEmptyField, + builder.CreateVector( + VectorOfKeyValuePairs{ + CreateTreeKeyValuePair( + builder, + builder.CreateString("aaa"), + CreateVariantTreeNode( + builder, + builder.CreateVector(VariantToFlexbuffer(100)), + kFlatbufferEmptyField)) + }))), + true, + false)); + // clang-format on + + const persistence::PersistedUserWriteRecord* persisted_user_write_record = + persistence::GetPersistedUserWriteRecord(builder.GetBufferPointer()); + UserWriteRecord result = + UserWriteRecordFromFlatbuffer(persisted_user_write_record); + + UserWriteRecord expected_result( + 1234, Path("this/is/a/path/to/a/thing"), + CompoundWrite::FromPathMerge( + std::map{{Path("aaa"), Variant(100)}})); + + EXPECT_EQ(result, expected_result); +} + +TEST(FlatBufferConversion, FlatbufferFromPersistedCompoundWrite) { + FlatBufferBuilder builder; + + FinishPersistedCompoundWriteBuffer( + builder, + FlatbufferFromCompoundWrite( + &builder, CompoundWrite::FromPathMerge(std::map{ + {Path("aaa/bbb"), Variant(100)}}))); + + const persistence::PersistedCompoundWrite* result = + persistence::GetPersistedCompoundWrite(builder.GetBufferPointer()); + + EXPECT_EQ(result->write_tree()->value(), nullptr); + + EXPECT_EQ(result->write_tree()->children()->size(), 1); + const persistence::TreeKeyValuePair* node_aaa = + result->write_tree()->children()->Get(0); + EXPECT_STREQ(node_aaa->key()->c_str(), "aaa"); + EXPECT_EQ(node_aaa->subtree()->value(), nullptr); + + EXPECT_EQ(node_aaa->subtree()->children()->size(), 1); + const persistence::TreeKeyValuePair* node_bbb = + node_aaa->subtree()->children()->Get(0); + EXPECT_STREQ(node_bbb->key()->c_str(), "bbb"); + EXPECT_THAT(node_bbb->subtree()->value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(100))); +} + +TEST(FlatBufferConversion, FlatbufferFromQueryParams) { + QueryParams query_params; + query_params.order_by = QueryParams::kOrderByValue; + query_params.order_by_child = "order_by_child"; + query_params.start_at_value = Variant::FromInt64(1234); + query_params.start_at_child_key = "start_at"; + query_params.end_at_value = Variant::FromInt64(9876); + query_params.end_at_child_key = "end_at"; + query_params.equal_to_value = Variant::FromInt64(5555); + query_params.equal_to_child_key = "equal_to"; + query_params.limit_first = 3333; + query_params.limit_last = 6666; + FlatBufferBuilder builder; + + FinishPersistedQueryParamsBuffer( + builder, FlatbufferFromQueryParams(&builder, query_params)); + + const persistence::PersistedQueryParams* result = + persistence::GetPersistedQueryParams(builder.GetBufferPointer()); + + EXPECT_EQ(result->order_by(), persistence::OrderBy_Value); + EXPECT_STREQ(result->order_by_child()->c_str(), "order_by_child"); + EXPECT_THAT(result->start_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(1234))); + EXPECT_STREQ(result->start_at_child_key()->c_str(), "start_at"); + EXPECT_THAT(result->end_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(9876))); + EXPECT_STREQ(result->end_at_child_key()->c_str(), "end_at"); + EXPECT_THAT(result->equal_to_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(5555))); + EXPECT_STREQ(result->equal_to_child_key()->c_str(), "equal_to"); + EXPECT_EQ(result->limit_first(), 3333); + EXPECT_EQ(result->limit_last(), 6666); +} + +TEST(FlatBufferConversion, FlatbufferFromQuerySpec) { + Path path("this/is/a/test/path"); + QueryParams query_params; + query_params.order_by = QueryParams::kOrderByValue; + query_params.order_by_child = "order_by_child"; + query_params.start_at_value = Variant::FromInt64(1234); + query_params.start_at_child_key = "start_at"; + query_params.end_at_value = Variant::FromInt64(9876); + query_params.end_at_child_key = "end_at"; + query_params.equal_to_value = Variant::FromInt64(5555); + query_params.equal_to_child_key = "equal_to"; + query_params.limit_first = 3333; + query_params.limit_last = 6666; + FlatBufferBuilder builder; + QuerySpec query_spec(path, query_params); + + FinishPersistedQuerySpecBuffer(builder, + FlatbufferFromQuerySpec(&builder, query_spec)); + + const persistence::PersistedQuerySpec* result = + persistence::GetPersistedQuerySpec(builder.GetBufferPointer()); + + EXPECT_STREQ(result->path()->c_str(), "this/is/a/test/path"); + EXPECT_EQ(result->params()->order_by(), persistence::OrderBy_Value); + EXPECT_STREQ(result->params()->order_by_child()->c_str(), "order_by_child"); + EXPECT_THAT(result->params()->start_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(1234))); + EXPECT_STREQ(result->params()->start_at_child_key()->c_str(), "start_at"); + EXPECT_THAT(result->params()->end_at_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(9876))); + EXPECT_STREQ(result->params()->end_at_child_key()->c_str(), "end_at"); + EXPECT_THAT(result->params()->equal_to_value_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer(5555))); + EXPECT_STREQ(result->params()->equal_to_child_key()->c_str(), "equal_to"); + EXPECT_EQ(result->params()->limit_first(), 3333); + EXPECT_EQ(result->params()->limit_last(), 6666); +} + +TEST(FlatBufferConversion, FlatbufferFromTrackedQuery) { + FlatBufferBuilder builder; + TrackedQuery tracked_query; + tracked_query.query_id = 100; + tracked_query.query_spec.path = Path("aaa/bbb/ccc"); + tracked_query.query_spec.params.order_by = QueryParams::kOrderByValue; + tracked_query.last_use = 1234; + tracked_query.complete = true; + tracked_query.active = true; + + FinishPersistedTrackedQueryBuffer( + builder, FlatbufferFromTrackedQuery(&builder, tracked_query)); + + const persistence::PersistedTrackedQuery* result = + persistence::GetPersistedTrackedQuery(builder.GetBufferPointer()); + + EXPECT_EQ(result->query_id(), 100); + EXPECT_STREQ(result->query_spec()->path()->c_str(), "aaa/bbb/ccc"); + EXPECT_EQ(result->query_spec()->params()->order_by(), + persistence::OrderBy_Value); + EXPECT_EQ(result->last_use(), 1234); + EXPECT_TRUE(result->complete()); + EXPECT_TRUE(result->active()); +} + +TEST(FlatBufferConversion, FlatbufferFromUserWriteRecord) { + FlatBufferBuilder builder; + UserWriteRecord user_write_record; + user_write_record.write_id = 123; + user_write_record.path = Path("aaa/bbb/ccc"); + user_write_record.overwrite = Variant("this is a string"); + user_write_record.visible = true; + user_write_record.is_overwrite = true; + + FinishPersistedUserWriteRecordBuffer( + builder, FlatbufferFromUserWriteRecord(&builder, user_write_record)); + const persistence::PersistedUserWriteRecord* result = + persistence::GetPersistedUserWriteRecord(builder.GetBufferPointer()); + + EXPECT_EQ(result->write_id(), 123); + EXPECT_STREQ(result->path()->c_str(), "aaa/bbb/ccc"); + EXPECT_THAT(result->overwrite_flexbuffer_root(), + EqualsFlexbuffer(VariantToFlexbuffer("this is a string"))); + EXPECT_EQ(result->visible(), true); + EXPECT_EQ(result->is_overwrite(), true); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc b/database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc new file mode 100644 index 0000000000..da0982d14c --- /dev/null +++ b/database/tests/desktop/persistence/in_memory_persistence_storage_engine_test.cc @@ -0,0 +1,415 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/persistence/in_memory_persistence_storage_engine.h" + +#include "app/src/logger.h" +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/compound_write.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(InMemoryPersistenceStorageEngine, Constructor) { + SystemLogger logger; + InMemoryPersistenceStorageEngine engine(&logger); + + // Ensure there is no crash. + (void)engine; +} + +class InMemoryPersistenceStorageEngineTest : public ::testing::Test { + public: + InMemoryPersistenceStorageEngineTest() : logger_(), engine_(&logger_) {} + + ~InMemoryPersistenceStorageEngineTest() override {} + + protected: + SystemLogger logger_; + InMemoryPersistenceStorageEngine engine_; +}; + +typedef InMemoryPersistenceStorageEngineTest + InMemoryPersistenceStorageEngineDeathTest; + +TEST_F(InMemoryPersistenceStorageEngineTest, LoadServerCache) { + // This is all in-memory, so nothing to read from disk. + EXPECT_EQ(engine_.LoadServerCache(), Variant::Null()); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveUserOverwrite) { + EXPECT_DEATH(engine_.SaveUserOverwrite(Path(), Variant::Null(), 100), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, SaveUserOverwrite) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.SaveUserOverwrite(Path(), Variant::Null(), 100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveUserMerge) { + EXPECT_DEATH(engine_.SaveUserMerge(Path(), CompoundWrite(), 100), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, SaveUserMerge) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.SaveUserMerge(Path(), CompoundWrite(), 100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, RemoveUserWrite) { + EXPECT_DEATH(engine_.RemoveUserWrite(100), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, RemoveUserWrite) { + engine_.BeginTransaction(); + engine_.RemoveUserWrite(100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, LoadUserWrites) { + // This is all in-memory, so nothing to read from disk. + EXPECT_TRUE(engine_.LoadUserWrites().empty()); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, RemoveAllUserWrites) { + // Must be in a transaction. + EXPECT_DEATH(engine_.RemoveAllUserWrites(), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, RemoveAllUserWrites) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.RemoveAllUserWrites(); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, OverwriteServerCache) { + EXPECT_DEATH(engine_.OverwriteServerCache(Path(), Variant::Null()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, OverwriteServerCache) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaa/bbb/ccc"), 100); + engine_.OverwriteServerCache(Path("aaa/bbb/ddd"), 200); + engine_.OverwriteServerCache(Path("zzz/yyy/xxx"), 300); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ccc")), 100); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ddd")), 200); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb")), + Variant(std::map{{"ccc", 100}, {"ddd", 200}})); + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 100}, + {"ddd", 200}, + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }})); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, + MergeIntoServerCache_Variant) { + EXPECT_DEATH(engine_.MergeIntoServerCache(Path(), Variant::Null()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, MergeIntoServerCache_Variant) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaa/bbb/ccc"), 100); + engine_.OverwriteServerCache(Path("aaa/bbb/ddd"), 200); + engine_.OverwriteServerCache(Path("zzz/yyy/xxx"), 300); + + engine_.MergeIntoServerCache( + Path("aaa/bbb"), std::map{{"ccc", 400}, {"eee", 500}}); + + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ccc")), 400); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ddd")), 200); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/eee")), 500); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb")), + Variant(std::map{ + {"ccc", 400}, {"ddd", 200}, {"eee", 500}})); + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 400}, + {"ddd", 200}, + {"eee", 500} + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }})); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, + MergeIntoServerCache_CompoundWrite) { + EXPECT_DEATH(engine_.MergeIntoServerCache(Path(), CompoundWrite()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, + MergeIntoServerCache_CompoundWrite) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaa/bbb/ccc"), 100); + engine_.OverwriteServerCache(Path("aaa/bbb/ddd"), 200); + engine_.OverwriteServerCache(Path("zzz/yyy/xxx"), 300); + + CompoundWrite write; + write = write.AddWrite(Path("ccc"), 400); + write = write.AddWrite(Path("eee"), 500); + + engine_.MergeIntoServerCache(Path("aaa/bbb"), write); + + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ccc")), 400); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/ddd")), 200); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb/eee")), 500); + EXPECT_EQ(engine_.ServerCache(Path("aaa/bbb")), + Variant(std::map{ + {"ccc", 400}, {"ddd", 200}, {"eee", 500}})); + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 400}, + {"ddd", 200}, + {"eee", 500} + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }})); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineTest, ServerCacheEstimatedSizeInBytes) { + engine_.BeginTransaction(); + engine_.OverwriteServerCache(Path("aaaa/bbbb"), + Variant::FromMutableString("abcdefghijklm")); + engine_.OverwriteServerCache(Path("aaaa/cccc"), + Variant::FromMutableString("nopqrstuvwxyz")); + engine_.OverwriteServerCache(Path("aaaa/dddd"), Variant::FromInt64(12345)); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + const int kKeyLengths = 4; // The keys used above are 4 characters; + const int kValueLengths = 13; // The values used above are 13 characters; + EXPECT_EQ(engine_.ServerCacheEstimatedSizeInBytes(), + 9 * sizeof(Variant) + 4 * kKeyLengths + 2 * kValueLengths); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveTrackedQuery) { + // Must be in a transaction. + EXPECT_DEATH(engine_.SaveTrackedQuery(TrackedQuery()), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, SaveTrackedQuery) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.SaveTrackedQuery(TrackedQuery()); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, DeleteTrackedQuery) { + // Must be in a transaction. + EXPECT_DEATH(engine_.DeleteTrackedQuery(100), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, DeleteTrackedQuery) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.DeleteTrackedQuery(100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, LoadTrackedQueries) { + // This is all in-memory, so nothing to read from disk. + EXPECT_TRUE(engine_.LoadTrackedQueries().empty()); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, PruneCache) { + engine_.BeginTransaction(); + // clang-format off + engine_.OverwriteServerCache( + Path(), + std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 100}, + {"ddd", 200} + }} + }}, + {"zzz", std::map{ + {"yyy", std::map{ + {"xxx", 300}} + }} + }}); + // clang-format on + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); + + PruneForest forest; + PruneForestRef ref(&forest); + + ref.Prune(Path("aaa/bbb")); + ref.Keep(Path("aaa/bbb/ccc")); + ref.Prune(Path("zzz")); + + engine_.PruneCache(Path(), ref); + + // clang-format off + EXPECT_EQ( + engine_.ServerCache(Path()), + Variant(std::map{ + {"aaa", std::map{ + {"bbb", std::map{ + {"ccc", 100} + }} + }} + })) << util::VariantToJson(engine_.ServerCache(Path())); + // clang-format on +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, + ResetPreviouslyActiveTrackedQueries) { + // Must be in a transaction. + EXPECT_DEATH(engine_.ResetPreviouslyActiveTrackedQueries(100), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, + ResetPreviouslyActiveTrackedQueries) { + // This is all in-memory, so nothing to save to disk. + // There is nothing to check except that it doesn't crash. + engine_.BeginTransaction(); + engine_.ResetPreviouslyActiveTrackedQueries(100); + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, SaveTrackedQueryKeys) { + // Must be in a transaction. + EXPECT_DEATH(engine_.SaveTrackedQueryKeys(100, std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, UpdateTrackedQueryKeys) { + EXPECT_DEATH(engine_.UpdateTrackedQueryKeys(100, std::set(), + std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, TrackedQueryKeys) { + engine_.BeginTransaction(); + + EXPECT_TRUE(engine_.LoadTrackedQueryKeys(100).empty()); + + engine_.SaveTrackedQueryKeys(100, {"aaa", "bbb", "ccc"}); + engine_.SaveTrackedQueryKeys(200, {"zzz", "yyy", "xxx"}); + + EXPECT_EQ(engine_.LoadTrackedQueryKeys(100), + std::set({"aaa", "bbb", "ccc"})); + EXPECT_EQ(engine_.LoadTrackedQueryKeys(200), + std::set({"zzz", "yyy", "xxx"})); + EXPECT_TRUE(engine_.LoadTrackedQueryKeys(300).empty()); + + engine_.UpdateTrackedQueryKeys(100, std::set({"ddd", "eee"}), + std::set({"aaa", "bbb"})); + + EXPECT_EQ(engine_.LoadTrackedQueryKeys(100), + std::set({"ccc", "ddd", "eee"})); + EXPECT_EQ(engine_.LoadTrackedQueryKeys(200), + std::set({"zzz", "yyy", "xxx"})); + EXPECT_TRUE(engine_.LoadTrackedQueryKeys(300).empty()); + + EXPECT_EQ(engine_.LoadTrackedQueryKeys(std::set({100})), + std::set({"ccc", "ddd", "eee"})); + EXPECT_EQ(engine_.LoadTrackedQueryKeys(std::set({100, 200})), + std::set({"ccc", "ddd", "eee", "zzz", "yyy", "xxx"})); + + engine_.SetTransactionSuccessful(); + engine_.EndTransaction(); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, BeginTransaction) { + EXPECT_TRUE(engine_.BeginTransaction()); + // Cannot begin a transaction while in a transaction. + EXPECT_DEATH(engine_.BeginTransaction(), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, BeginTransaction) { + // BeginTransaction should return true, indicating success. + EXPECT_TRUE(engine_.BeginTransaction()); +} + +TEST_F(InMemoryPersistenceStorageEngineDeathTest, EndTransaction) { + // Cannot end a transaction unless in a transaction. + EXPECT_DEATH(engine_.EndTransaction(), DEATHTEST_SIGABRT); +} + +TEST_F(InMemoryPersistenceStorageEngineTest, EndTransaction) { + EXPECT_TRUE(engine_.BeginTransaction()); + engine_.EndTransaction(); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc b/database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc new file mode 100644 index 0000000000..4bcfbc974e --- /dev/null +++ b/database/tests/desktop/persistence/level_db_persistence_storage_engine_test.cc @@ -0,0 +1,700 @@ +// Copyright 2020 Google LLC +// +// 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 "database/src/desktop/persistence/level_db_persistence_storage_engine.h" + +#include +#include +#include + +#include "app/src/logger.h" +#include "app/src/variant_util.h" +#include "database/src/desktop/persistence/flatbuffer_conversions.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// TODO(amablue): Consider refactoring this into a common location. +#if defined(_WIN32) +static const char kDirectorySeparator[] = "\\"; +#else +static const char kDirectorySeparator[] = "/"; +#endif // defined(_WIN32) + +static std::string GetTestTmpDir(const char test_namespace[]) { +#if defined(_WIN32) + char buf[MAX_PATH + 1]; + if (GetEnvironmentVariableA("TEST_TMPDIR", buf, sizeof(buf))) { + return std::string(buf) + kDirectorySeparator + test_namespace; + } +#else + // Linux and OS X should either have the TEST_TMPDIR environment variable set. + if (const char* value = getenv("TEST_TMPDIR")) { + return std::string(value) + kDirectorySeparator + test_namespace; + } +#endif // defined(_WIN32) + // If we weren't able to get TEST_TMPDIR, just use a subdirectory. + return test_namespace; +} + +TEST(LevelDbPersistenceStorageEngine, ConstructorBasic) { + const std::string kDatabaseFilename = GetTestTmpDir(test_info_->name()); + + // Just ensure that nothing crashes. + SystemLogger logger; + LevelDbPersistenceStorageEngine engine(&logger); + engine.Initialize(kDatabaseFilename); +} + +class LevelDbPersistenceStorageEngineTest : public ::testing::Test { + protected: + void SetUp() override { + engine_ = new LevelDbPersistenceStorageEngine(&logger_); + } + + void TearDown() override { delete engine_; } + + // All tests should start with this. This sets the path Level DB should read + // from and write to, and caches that path so that when we re-start Level DB + // we have the path we used on the previous run. + void InitializeLevelDb(const std::string& test_name) { + database_path_ = GetTestTmpDir(test_name.c_str()); + engine_->Initialize(database_path_); + } + + // We want to run all of our tests twice: Once immediately after the functions + // have been called on the database, and then once again after the database + // has been shut down and restarted. + template + void RunTwice(const Func& func) { + func(); + TearDown(); + SetUp(); + engine_->Initialize(database_path_); + func(); + } + + SystemLogger logger_; + LevelDbPersistenceStorageEngine* engine_; + std::string database_path_; +}; + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveUserOverwrite) { + InitializeLevelDb(test_info_->name()); + + Path path_a("aaa/bbb"); + Variant data_a("variant_data"); + WriteId write_id_a = 100; + + Path path_b("ccc/ddd"); + Variant data_b("variant_data_two"); + WriteId write_id_b = 101; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserOverwrite(path_a, data_a, write_id_a); + engine_->SaveUserOverwrite(path_b, data_b, write_id_b); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{ + UserWriteRecord(100, Path("aaa/bbb"), "variant_data", true), + UserWriteRecord(101, Path("ccc/ddd"), "variant_data_two", true)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveUserMerge) { + InitializeLevelDb(test_info_->name()); + + Path path("this/is/a/test/path"); + CompoundWrite children = CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("larry"), 999), + std::make_pair(Path("curly"), 888), + std::make_pair(Path("moe"), 777), + }); + WriteId write_id = 100; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserMerge(path, children, write_id); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{ + UserWriteRecord(100, Path("this/is/a/test/path"), + CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("larry"), 999), + std::make_pair(Path("curly"), 888), + std::make_pair(Path("moe"), 777), + }))}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, RemoveUserWrite) { + InitializeLevelDb(test_info_->name()); + + Path path_a("this/is/a/test/path"); + Variant data_a("variant_data"); + WriteId write_id_a = 100; + + Path path_b("this/is/another/test/path"); + Variant data_b("variant_data_two"); + WriteId write_id_b = 101; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserOverwrite(path_a, data_a, write_id_a); + engine_->SaveUserOverwrite(path_b, data_b, write_id_b); + engine_->RemoveUserWrite(100); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{ + UserWriteRecord(101, Path("this/is/another/test/path"), + Variant("variant_data_two"), true)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, RemoveAllUserWrites) { + InitializeLevelDb(test_info_->name()); + + Path path_a("this/is/a/test/path"); + Variant data_a("variant_data"); + WriteId write_id_a = 100; + + Path path_b("this/is/another/test/path"); + Variant data_b("variant_data_two"); + WriteId write_id_b = 101; + + // Compare to ensure the written value is the expected value. + engine_->BeginTransaction(); + engine_->SaveUserOverwrite(path_a, data_a, write_id_a); + engine_->SaveUserOverwrite(path_b, data_b, write_id_b); + engine_->RemoveAllUserWrites(); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadUserWrites(); + std::vector expected{}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, OverwriteServerCache) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path("aaa/bbb"), Variant("some value")); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + Variant expected("some value"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa")); + // clang-format off + Variant expected = std::map{ + std::make_pair("bbb", "some value"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("aaa", std::map{ + std::make_pair("bbb", "some value"), + }) + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, OverwriteServerCache_Overwrite) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path("aaa/bbb"), Variant("some value")); + engine_->OverwriteServerCache(Path("aaa"), Variant("Overwrite!")); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + Variant expected = Variant::Null(); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa")); + Variant expected("Overwrite!"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("aaa", "Overwrite!"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, MergeIntoServerCacheWithVariant) { + InitializeLevelDb(test_info_->name()); + + Variant merge = std::map{ + std::make_pair("ccc", std::map{std::make_pair( + "ddd", "some value")}), + std::make_pair("eee", "adjacent value"), + }; + + engine_->BeginTransaction(); + engine_->MergeIntoServerCache(Path("aaa/bbb"), merge); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb/ccc/ddd")); + Variant expected = "some value"; + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb/eee")); + Variant expected("adjacent value"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + // clang-format off + Variant expected = std::map{ + std::make_pair("ccc", std::map{ + std::make_pair("ddd", "some value"), + }), + std::make_pair("eee", "adjacent value"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, + MergeIntoServerCacheWithCompoundWrite) { + InitializeLevelDb(test_info_->name()); + + CompoundWrite merge = CompoundWrite::FromPathMerge(std::map{ + std::make_pair(Path("ccc/ddd"), "some value"), + std::make_pair(Path("eee"), "adjacent value"), + }); + + engine_->BeginTransaction(); + engine_->MergeIntoServerCache(Path("aaa/bbb"), merge); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + Variant result = engine_->ServerCache(Path("aaa/bbb/ccc/ddd")); + Variant expected = "some value"; + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb/eee")); + Variant expected("adjacent value"); + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path("aaa/bbb")); + // clang-format off + Variant expected = std::map{ + std::make_pair("ccc", std::map{ + std::make_pair("ddd", "some value"), + }), + std::make_pair("eee", "adjacent value"), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("aaa", std::map{ + std::make_pair("bbb", std::map{ + std::make_pair("ccc", std::map{ + std::make_pair("ddd", "some value"), + }), + std::make_pair("eee", "adjacent value"), + }), + }), + }; + // clang-format on + EXPECT_EQ(result, expected); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, ServerCacheEstimatedSizeInBytes) { + InitializeLevelDb(test_info_->name()); + + std::string long_string(1024, 'x'); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path("aaa"), long_string); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + uint64 result = engine_->ServerCacheEstimatedSizeInBytes(); + uint64 expected = 1024 + strlen("aaa"); + + // This is only an estimate, so as long as we're within a few bytes it's + // okay. + EXPECT_NEAR(result, expected, 16); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveTrackedQuery) { + InitializeLevelDb(test_info_->name()); + + TrackedQuery tracked_query_a(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive); + TrackedQuery tracked_query_b(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + + engine_->BeginTransaction(); + engine_->SaveTrackedQuery(tracked_query_a); + engine_->SaveTrackedQuery(tracked_query_b); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadTrackedQueries(); + std::vector expected{ + TrackedQuery(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive), + TrackedQuery(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, TrackedQuery::kInactive)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, DeleteTrackedQuery) { + InitializeLevelDb(test_info_->name()); + + TrackedQuery tracked_query_a(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive); + TrackedQuery tracked_query_b(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + + engine_->BeginTransaction(); + engine_->SaveTrackedQuery(tracked_query_a); + engine_->SaveTrackedQuery(tracked_query_b); + engine_->DeleteTrackedQuery(100); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadTrackedQueries(); + std::vector expected{ + TrackedQuery(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, TrackedQuery::kInactive)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, + ResetPreviouslyActiveTrackedQueries) { + InitializeLevelDb(test_info_->name()); + + TrackedQuery tracked_query_a(100, QuerySpec(Path("aaa/bbb/ccc")), 1234, + TrackedQuery::kComplete, TrackedQuery::kActive); + TrackedQuery tracked_query_b(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, + TrackedQuery::kInactive); + + engine_->BeginTransaction(); + engine_->SaveTrackedQuery(tracked_query_a); + engine_->SaveTrackedQuery(tracked_query_b); + engine_->ResetPreviouslyActiveTrackedQueries(9999); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + std::vector result = engine_->LoadTrackedQueries(); + std::vector expected{ + TrackedQuery(100, QuerySpec(Path("aaa/bbb/ccc")), 9999, + TrackedQuery::kComplete, TrackedQuery::kInactive), + TrackedQuery(101, QuerySpec(Path("aaa/bbb/ddd")), 5678, + TrackedQuery::kIncomplete, TrackedQuery::kInactive)}; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, SaveTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->SaveTrackedQueryKeys(100, + std::set{"key1", "key2", "key3"}); + engine_->SaveTrackedQueryKeys(101, + std::set{"key4", "key5", "key6"}); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + std::set result = engine_->LoadTrackedQueryKeys(100); + std::set expected{"key1", "key2", "key3"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = engine_->LoadTrackedQueryKeys(101); + std::set expected{"key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = + engine_->LoadTrackedQueryKeys(std::set{100, 101}); + std::set expected{"key1", "key2", "key3", + "key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, UpdateTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + + engine_->BeginTransaction(); + engine_->SaveTrackedQueryKeys(100, + std::set{"key1", "key2", "key3"}); + engine_->SaveTrackedQueryKeys(101, + std::set{"key4", "key5", "key6"}); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + { + std::set result = engine_->LoadTrackedQueryKeys(100); + std::set expected{"key1", "key2", "key3"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = engine_->LoadTrackedQueryKeys(101); + std::set expected{"key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + { + std::set result = + engine_->LoadTrackedQueryKeys(std::set{100, 101}); + std::set expected{"key1", "key2", "key3", + "key4", "key5", "key6"}; + EXPECT_THAT(result, Pointwise(Eq(), expected)); + } + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, PruneCache) { + InitializeLevelDb(test_info_->name()); + + // clang-format off + Variant initial_data = std::map{ + std::make_pair("the_root", std::map{ + std::make_pair("delete_me", std::map{ + std::make_pair("but_keep_me", 111), + std::make_pair("ill_be_gone", 222), + }), + std::make_pair("keep_me", std::map{ + std::make_pair("but_delete_me", 333), + std::make_pair("ill_be_here", 444), + }), + }), + }; + // clang-format on + + PruneForest prune_forest; + PruneForestRef prune_forest_ref(&prune_forest); + prune_forest_ref.Prune(Path("delete_me")); + prune_forest_ref.Keep(Path("delete_me/but_keep_me")); + prune_forest_ref.Prune(Path("keep_me/but_delete_me")); + + engine_->BeginTransaction(); + engine_->OverwriteServerCache(Path(), initial_data); + engine_->PruneCache(Path("the_root"), prune_forest_ref); + engine_->SetTransactionSuccessful(); + engine_->EndTransaction(); + + RunTwice([this]() { + Variant result = engine_->ServerCache(Path()); + // clang-format off + Variant expected = std::map{ + std::make_pair("the_root", std::map{ + std::make_pair("delete_me", std::map{ + std::make_pair("but_keep_me", 111), + }), + std::make_pair("keep_me", std::map{ + std::make_pair("ill_be_here", 444), + }), + }), + }; + // clang-format on + EXPECT_EQ(result, expected); + }); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, BeginTransaction) { + // BeginTransaction should return true, indicating success. + EXPECT_TRUE(engine_->BeginTransaction()); +} + +TEST_F(LevelDbPersistenceStorageEngineTest, EndTransaction) { + EXPECT_TRUE(engine_->BeginTransaction()); + engine_->EndTransaction(); +} + +// Many functions are designed to assert if called outside a transaction. Ensure +// they crash as expected. +using LevelDbPersistenceStorageEngineDeathTest = + LevelDbPersistenceStorageEngineTest; + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveUserOverwrite) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveUserOverwrite(Path(), Variant(), 0), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveUserMerge) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveUserMerge(Path(), CompoundWrite(), 0), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, RemoveUserWrite) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->RemoveUserWrite(0), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, RemoveAllUserWrites) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->RemoveAllUserWrites(), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, OverwriteServerCache) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->OverwriteServerCache(Path(), Variant()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, MergeIntoServerCacheVariant) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->MergeIntoServerCache(Path(), Variant()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, + MergeIntoServerCacheCompoundWrite) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->MergeIntoServerCache(Path(), CompoundWrite()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveTrackedQuery) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveTrackedQuery(TrackedQuery()), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, DeleteTrackedQuery) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->DeleteTrackedQuery(0), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, + ResetPreviouslyActiveTrackedQueries) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->ResetPreviouslyActiveTrackedQueries(0), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, SaveTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->SaveTrackedQueryKeys(0, std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, UpdateTrackedQueryKeys) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->UpdateTrackedQueryKeys(0, std::set(), + std::set()), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, PruneCache) { + InitializeLevelDb(test_info_->name()); + EXPECT_DEATH(engine_->PruneCache(Path(), PruneForestRef(nullptr)), + DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, BeginTransaction) { + EXPECT_TRUE(engine_->BeginTransaction()); + // Cannot begin a transaction while in a transaction. + EXPECT_DEATH(engine_->BeginTransaction(), DEATHTEST_SIGABRT); +} + +TEST_F(LevelDbPersistenceStorageEngineDeathTest, EndTransaction) { + // Cannot end a transaction unless in a transaction. + EXPECT_DEATH(engine_->EndTransaction(), DEATHTEST_SIGABRT); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/noop_persistence_manager_test.cc b/database/tests/desktop/persistence/noop_persistence_manager_test.cc new file mode 100644 index 0000000000..a60fcfa8c5 --- /dev/null +++ b/database/tests/desktop/persistence/noop_persistence_manager_test.cc @@ -0,0 +1,86 @@ +// Copyright 2020 Google LLC +// +// 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 "database/src/desktop/persistence/noop_persistence_manager.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(NoopPersistenceManager, Constructor) { + // Ensure there is no crash. + NoopPersistenceManager manager; + (void)manager; +} + +TEST(NoopPersistenceManager, LoadUserWrites) { + NoopPersistenceManager manager; + EXPECT_TRUE(manager.LoadUserWrites().empty()); +} + +TEST(NoopPersistenceManager, ServerCache) { + NoopPersistenceManager manager; + EXPECT_EQ(manager.ServerCache(QuerySpec()), CacheNode()); +} + +TEST(NoopPersistenceManager, InsideTransaction) { + // Make sure none of these functions result in a crash. There is no state we + // can query or other side effects that we can test. + NoopPersistenceManager manager; + EXPECT_TRUE(manager.RunInTransaction([&manager]() { + manager.SaveUserMerge(Path(), CompoundWrite(), 100); + manager.RemoveUserWrite(100); + manager.RemoveAllUserWrites(); + manager.ApplyUserWriteToServerCache(Path("a/b/c"), Variant::FromInt64(123)); + manager.ApplyUserWriteToServerCache(Path("a/b/c"), CompoundWrite()); + manager.UpdateServerCache(QuerySpec(), Variant::FromInt64(123)); + manager.UpdateServerCache(Path("a/b/c"), CompoundWrite()); + manager.SetQueryActive(QuerySpec()); + manager.SetQueryInactive(QuerySpec()); + manager.SetQueryComplete(QuerySpec()); + manager.SetTrackedQueryKeys(QuerySpec(), + std::set{"aaa", "bbb"}); + manager.UpdateTrackedQueryKeys(QuerySpec(), + std::set{"aaa", "bbb"}, + std::set{"ccc", "ddd"}); + return true; + })); +} + +TEST(NoopPersistenceManagerDeathTest, NestedTransaction) { + // Make sure none of these functions result in a crash. There is no state we + // can query or other side effects that we can test. + NoopPersistenceManager manager; + EXPECT_DEATH(manager.RunInTransaction([&manager]() { + // This transaction should run. + manager.RunInTransaction([]() { + // This transaction should not run, since the nested call to + // RunInTransaction should assert. + return true; + }); + return true; + }), + DEATHTEST_SIGABRT); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/persistence_manager_test.cc b/database/tests/desktop/persistence/persistence_manager_test.cc new file mode 100644 index 0000000000..99efc673b0 --- /dev/null +++ b/database/tests/desktop/persistence/persistence_manager_test.cc @@ -0,0 +1,461 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/persistence/persistence_manager.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/tests/desktop/test/mock_cache_policy.h" +#include "database/tests/desktop/test/mock_persistence_storage_engine.h" +#include "database/tests/desktop/test/mock_tracked_query_manager.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::StrictMock; +using testing::Test; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +class PersistenceManagerTest : public Test { + public: + void SetUp() override { + storage_engine_ = new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine_); + + tracked_query_manager_ = new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager_); + + cache_policy_ = new NiceMock(); + UniquePtr cache_policy_ptr(cache_policy_); + + manager_ = new PersistenceManager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger_); + } + + void TearDown() override { delete manager_; } + + protected: + MockPersistenceStorageEngine* storage_engine_; + MockTrackedQueryManager* tracked_query_manager_; + MockCachePolicy* cache_policy_; + SystemLogger logger_; + + PersistenceManager* manager_; +}; + +TEST_F(PersistenceManagerTest, SaveUserOverwrite) { + EXPECT_CALL( + *storage_engine_, + SaveUserOverwrite(Path("test/path"), Variant("test_variant"), 100)); + + manager_->SaveUserOverwrite(Path("test/path"), Variant("test_variant"), 100); +} + +TEST_F(PersistenceManagerTest, SaveUserMerge) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_CALL(*storage_engine_, SaveUserMerge(Path("test/path"), write, 100)); + + manager_->SaveUserMerge(Path("test/path"), write, 100); +} + +TEST_F(PersistenceManagerTest, RemoveUserWrite) { + EXPECT_CALL(*storage_engine_, RemoveUserWrite(100)); + + manager_->RemoveUserWrite(100); +} + +TEST_F(PersistenceManagerTest, RemoveAllUserWrites) { + EXPECT_CALL(*storage_engine_, RemoveAllUserWrites()); + + manager_->RemoveAllUserWrites(); +} + +TEST_F(PersistenceManagerTest, ApplyUserWriteToServerCacheWithoutActiveQuery) { + // If there is no active default query, we expect it to apply the variant to + // the storage engine at the given path. + EXPECT_CALL(*tracked_query_manager_, HasActiveDefaultQuery(Path("abc"))) + .WillOnce(Return(false)); + EXPECT_CALL(*storage_engine_, + OverwriteServerCache(Path("abc"), Variant("zyx"))); + EXPECT_CALL(*tracked_query_manager_, EnsureCompleteTrackedQuery(Path("abc"))); + + manager_->ApplyUserWriteToServerCache(Path("abc"), "zyx"); +} + +TEST_F(PersistenceManagerTest, ApplyUserWriteToServerCacheWithActiveQuery) { + // If there is an active default query, nothing should happen. + EXPECT_CALL(*tracked_query_manager_, HasActiveDefaultQuery(Path("abc"))) + .WillOnce(Return(true)); + + manager_->ApplyUserWriteToServerCache(Path("abc"), Variant("zyx")); +} + +TEST_F(PersistenceManagerTest, ApplyUserWriteToServerCacheWithCompoundWrite) { + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_CALL(*tracked_query_manager_, HasActiveDefaultQuery(_)) + .WillRepeatedly(Return(false)); + + EXPECT_CALL(*storage_engine_, OverwriteServerCache(Path("aaa"), Variant(1))); + EXPECT_CALL(*tracked_query_manager_, EnsureCompleteTrackedQuery(Path("aaa"))); + + EXPECT_CALL(*storage_engine_, OverwriteServerCache(Path("bbb"), Variant(2))); + EXPECT_CALL(*tracked_query_manager_, EnsureCompleteTrackedQuery(Path("bbb"))); + + EXPECT_CALL(*storage_engine_, + OverwriteServerCache(Path("ccc/ddd"), Variant(3))); + EXPECT_CALL(*tracked_query_manager_, + EnsureCompleteTrackedQuery(Path("ccc/ddd"))); + + EXPECT_CALL(*storage_engine_, + OverwriteServerCache(Path("ccc/eee"), Variant(4))); + EXPECT_CALL(*tracked_query_manager_, + EnsureCompleteTrackedQuery(Path("ccc/eee"))); + + manager_->ApplyUserWriteToServerCache(Path(), write); +} + +TEST_F(PersistenceManagerTest, LoadUserWrites) { + EXPECT_CALL(*storage_engine_, LoadUserWrites()); + manager_->LoadUserWrites(); +} + +TEST_F(PersistenceManagerTest, ServerCache_QueryComplete) { + QuerySpec query_spec; + query_spec.params.start_at_value = "zzz"; + query_spec.path = Path("abc"); + + TrackedQuery tracked_query; + tracked_query.query_id = 1234; + tracked_query.active = true; + tracked_query.complete = true; + + std::set tracked_keys{"aaa", "ccc"}; + + Variant server_cache(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }); + + EXPECT_CALL(*tracked_query_manager_, IsQueryComplete(query_spec)) + .WillOnce(Return(true)); + EXPECT_CALL(*tracked_query_manager_, FindTrackedQuery(query_spec)) + .WillOnce(Return(&tracked_query)); + EXPECT_CALL(*storage_engine_, LoadTrackedQueryKeys(1234)) + .WillOnce(Return(tracked_keys)); + EXPECT_CALL(*storage_engine_, ServerCache(Path("abc"))) + .WillOnce(Return(server_cache)); + + CacheNode result = manager_->ServerCache(query_spec); + CacheNode expected_result( + IndexedVariant(Variant(std::map{ + std::make_pair("aaa", 1), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }), + query_spec.params), + true, true); + + EXPECT_EQ(result, expected_result); +} + +TEST_F(PersistenceManagerTest, ServerCache_QueryIncomplete) { + QuerySpec query_spec; + query_spec.params.start_at_value = "zzz"; + query_spec.path = Path("abc"); + + TrackedQuery tracked_query; + tracked_query.query_id = 1234; + tracked_query.active = true; + tracked_query.complete = false; + + std::set tracked_keys{"aaa", "ccc"}; + + Variant server_cache(std::map{ + std::make_pair("aaa", 1), + std::make_pair("bbb", 2), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }); + + EXPECT_CALL(*tracked_query_manager_, IsQueryComplete(query_spec)) + .WillOnce(Return(false)); + EXPECT_CALL(*tracked_query_manager_, GetKnownCompleteChildren(Path("abc"))) + .WillOnce(Return(tracked_keys)); + EXPECT_CALL(*storage_engine_, ServerCache(Path("abc"))) + .WillOnce(Return(server_cache)); + + CacheNode result = manager_->ServerCache(query_spec); + CacheNode expected_result( + IndexedVariant(Variant(std::map{ + std::make_pair("aaa", 1), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 3), + std::make_pair("eee", 4), + }), + }), + query_spec.params), + false, true); + + EXPECT_EQ(result, expected_result); +} + +TEST_F(PersistenceManagerTest, UpdateServerCache_LoadsAllData) { + Path path; + Variant variant; + QuerySpec query_spec; + query_spec.path = path; + + EXPECT_CALL(*storage_engine_, OverwriteServerCache(path, variant)); + + manager_->UpdateServerCache(query_spec, variant); +} + +TEST_F(PersistenceManagerTest, UpdateServerCache_DoesntLoadAllData) { + Path path; + Variant variant; + QuerySpec query_spec; + query_spec.params.start_at_value = "bbb"; + query_spec.path = path; + + EXPECT_CALL(*storage_engine_, MergeIntoServerCache(path, variant)); + + manager_->UpdateServerCache(query_spec, variant); +} + +TEST_F(PersistenceManagerTest, UpdateServerCache_WithCompoundWrite) { + Path path; + + const std::map& merge{ + std::make_pair(Path("aaa"), 1), + std::make_pair(Path("bbb"), 2), + std::make_pair(Path("ccc/ddd"), 3), + std::make_pair(Path("ccc/eee"), 4), + }; + CompoundWrite write = CompoundWrite::FromPathMerge(merge); + + EXPECT_CALL(*storage_engine_, MergeIntoServerCache(path, write)); + + manager_->UpdateServerCache(path, write); +} + +TEST_F(PersistenceManagerTest, SetQueryActive) { + EXPECT_CALL(*tracked_query_manager_, + SetQueryActiveFlag(QuerySpec(), TrackedQuery::kActive)); + + manager_->SetQueryActive(QuerySpec()); +} + +TEST_F(PersistenceManagerTest, SetQueryInactive) { + EXPECT_CALL(*tracked_query_manager_, + SetQueryActiveFlag(QuerySpec(), TrackedQuery::kInactive)); + + manager_->SetQueryInactive(QuerySpec()); +} + +TEST_F(PersistenceManagerTest, SetQueryComplete) { + QuerySpec loads_all_data; + loads_all_data.path = Path("aaa"); + QuerySpec does_not_load_all_data; + does_not_load_all_data.path = Path("bbb"); + does_not_load_all_data.params.start_at_value = "abc"; + + EXPECT_CALL(*tracked_query_manager_, SetQueriesComplete(Path("aaa"))); + manager_->SetQueryComplete(loads_all_data); + + EXPECT_CALL(*tracked_query_manager_, + SetQueryCompleteIfExists(does_not_load_all_data)); + manager_->SetQueryComplete(does_not_load_all_data); +} + +TEST_F(PersistenceManagerTest, SetTrackedQueryKeys) { + QuerySpec query_spec; + query_spec.params.start_at_value = "baa"; + std::set keys{"foo", "bar", "baz"}; + + TrackedQuery tracked_query; + tracked_query.query_id = 1234; + tracked_query.active = true; + EXPECT_CALL(*tracked_query_manager_, FindTrackedQuery(query_spec)) + .WillOnce(Return(&tracked_query)); + EXPECT_CALL(*storage_engine_, SaveTrackedQueryKeys(1234, keys)); + + manager_->SetTrackedQueryKeys(query_spec, keys); +} + +TEST_F(PersistenceManagerTest, UpdateTrackedQueryKeys) { + QuerySpec query_spec; + query_spec.params.start_at_value = "baa"; + std::set added{"foo", "bar", "baz"}; + std::set removed{"qux", "quux", "quuz"}; + + TrackedQuery tracked_query; + tracked_query.query_id = 9876; + tracked_query.active = true; + EXPECT_CALL(*tracked_query_manager_, FindTrackedQuery(query_spec)) + .WillOnce(Return(&tracked_query)); + EXPECT_CALL(*storage_engine_, UpdateTrackedQueryKeys(9876, added, removed)); + + manager_->UpdateTrackedQueryKeys(query_spec, added, removed); +} + +TEST(PersistenceManager, DoPruneCheckAfterServerUpdate_DoNotCheckCacheSize) { + MockPersistenceStorageEngine* storage_engine = + new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine); + + MockTrackedQueryManager* tracked_query_manager = + new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager); + + MockCachePolicy* cache_policy = new StrictMock(); + UniquePtr cache_policy_ptr(cache_policy); + + SystemLogger logger; + PersistenceManager manager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger); + + // After the server cache is updated, DoPruneCheckAfterServerUpdate will be + // called. It should call CachePolicy::ShouldCheckCacheSize once, and if it + // returns false, it should not do anything else. + EXPECT_CALL(*cache_policy, ShouldCheckCacheSize(_)).WillOnce(Return(false)); + + manager.UpdateServerCache(QuerySpec(), Variant()); +} + +TEST(PersistenceManager, DoPruneCheckAfterServerUpdate_DoCheckCacheSize) { + MockPersistenceStorageEngine* storage_engine = + new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine); + + MockTrackedQueryManager* tracked_query_manager = + new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager); + + MockCachePolicy* cache_policy = new StrictMock(); + UniquePtr cache_policy_ptr(cache_policy); + + SystemLogger logger; + PersistenceManager manager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger); + + // After the server cache is updated, DoPruneCheckAfterServerUpdate will be + // called. It should call CachePolicy::ShouldCheckCacheSize once, and if it + // returns true, it will then check if it should prune anything. If + // CachePolicy::ShouldPrune returns false, nothing else will happen. + EXPECT_CALL(*cache_policy, ShouldCheckCacheSize(_)).WillOnce(Return(true)); + EXPECT_CALL(*cache_policy, ShouldPrune(_, _)).WillOnce(Return(false)); + + manager.UpdateServerCache(QuerySpec(), Variant()); +} + +TEST(PersistenceManager, DoPruneCheckAfterServerUpdate_PruneStuff) { + MockPersistenceStorageEngine* storage_engine = + new NiceMock(); + UniquePtr storage_engine_ptr(storage_engine); + + MockTrackedQueryManager* tracked_query_manager = + new NiceMock(); + UniquePtr tracked_query_manager_ptr( + tracked_query_manager); + + MockCachePolicy* cache_policy = new StrictMock(); + UniquePtr cache_policy_ptr(cache_policy); + + SystemLogger logger; + PersistenceManager manager(std::move(storage_engine_ptr), + std::move(tracked_query_manager_ptr), + std::move(cache_policy_ptr), &logger); + + // After the server cache is updated, DoPruneCheckAfterServerUpdate will be + // called. It should call CachePolicy::ShouldCheckCacheSize once, and if it + // returns true, it will then check if it should prune anything. If + // CachePolicy::ShouldPrune returns true, it will pass the prune tree to + // StorageEngine::PruneCache. + EXPECT_CALL(*cache_policy, ShouldCheckCacheSize(_)).WillOnce(Return(true)); + EXPECT_CALL(*cache_policy, ShouldPrune(_, _)) + .WillOnce(Return(true)) + .WillOnce(Return(false)); + PruneForest prune_forest; + prune_forest.set_value(true); + EXPECT_CALL(*tracked_query_manager, PruneOldQueries(_)) + .WillOnce(Return(prune_forest)); + EXPECT_CALL(*storage_engine, + PruneCache(Path(), PruneForestRef(&prune_forest))); + + manager.UpdateServerCache(QuerySpec(), Variant()); +} + +TEST_F(PersistenceManagerTest, RunInTransaction_StdFunctionSuccess) { + EXPECT_CALL(*storage_engine_, BeginTransaction()); + EXPECT_CALL(*storage_engine_, SetTransactionSuccessful()); + EXPECT_CALL(*storage_engine_, EndTransaction()); + bool function_called = false; + EXPECT_TRUE(manager_->RunInTransaction([&]() { + function_called = true; + return true; + })); + EXPECT_TRUE(function_called); +} + +TEST_F(PersistenceManagerTest, RunInTransaction_StdFunctionFailure) { + EXPECT_CALL(*storage_engine_, BeginTransaction()); + EXPECT_CALL(*storage_engine_, EndTransaction()); + bool function_called = false; + EXPECT_FALSE(manager_->RunInTransaction([&]() { + function_called = true; + return false; + })); + EXPECT_TRUE(function_called); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/persistence/prune_forest_test.cc b/database/tests/desktop/persistence/prune_forest_test.cc new file mode 100644 index 0000000000..efdf524d94 --- /dev/null +++ b/database/tests/desktop/persistence/prune_forest_test.cc @@ -0,0 +1,485 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/persistence/prune_forest.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(PruneForestTest, Equality) { + PruneForest forest; + forest.SetValueAt(Path("true"), true); + forest.SetValueAt(Path("false"), false); + + PruneForest identical_forest; + identical_forest.SetValueAt(Path("true"), true); + identical_forest.SetValueAt(Path("false"), false); + + PruneForest different_forest; + different_forest.SetValueAt(Path("true"), false); + different_forest.SetValueAt(Path("false"), true); + + PruneForestRef ref(&forest); + PruneForestRef same_ref(&forest); + PruneForestRef identical_ref(&identical_forest); + PruneForestRef different_ref(&different_forest); + PruneForestRef null_ref(nullptr); + PruneForestRef another_null_ref(nullptr); + + EXPECT_EQ(ref, ref); + EXPECT_EQ(ref, same_ref); + EXPECT_EQ(ref, identical_ref); + EXPECT_NE(ref, different_ref); + EXPECT_NE(ref, null_ref); + + EXPECT_EQ(null_ref, null_ref); + EXPECT_EQ(null_ref, another_null_ref); + EXPECT_EQ(another_null_ref, null_ref); +} + +TEST(PruneForestTest, PrunesAnything) { + { + PruneForest forest; + PruneForestRef ref(&forest); + EXPECT_FALSE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo")); + EXPECT_TRUE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo/bar/baz")); + EXPECT_TRUE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo")); + EXPECT_FALSE(ref.PrunesAnything()); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo/bar/baz")); + EXPECT_FALSE(ref.PrunesAnything()); + } +} + +TEST(PruneForestTest, ShouldPruneUnkeptDescendants) { + { + PruneForest forest; + PruneForestRef ref(&forest); + + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path())); + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("aaa"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("bbb"), false); + + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path())); + EXPECT_TRUE(ref.ShouldPruneUnkeptDescendants(Path("aaa"))); + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("bbb"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), false); + forest.SetValueAt(Path("aaa/bbb"), true); + forest.SetValueAt(Path("aaa/bbb/ccc"), false); + + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("aaa"))); + EXPECT_TRUE(ref.ShouldPruneUnkeptDescendants(Path("aaa/bbb"))); + EXPECT_FALSE(ref.ShouldPruneUnkeptDescendants(Path("aaa/bbb/ccc"))); + } +} + +TEST(PruneForestTest, ShouldKeep) { + { + PruneForest forest; + PruneForestRef ref(&forest); + + EXPECT_FALSE(ref.ShouldKeep(Path())); + EXPECT_FALSE(ref.ShouldKeep(Path("aaa"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("bbb"), false); + + EXPECT_FALSE(ref.ShouldKeep(Path())); + EXPECT_FALSE(ref.ShouldKeep(Path("aaa"))); + EXPECT_TRUE(ref.ShouldKeep(Path("bbb"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("aaa/bbb"), false); + + EXPECT_FALSE(ref.ShouldKeep(Path("aaa"))); + EXPECT_TRUE(ref.ShouldKeep(Path("aaa/bbb"))); + } +} + +TEST(PruneForestTest, AffectsPath) { + { + PruneForest forest; + PruneForestRef ref(&forest); + + EXPECT_FALSE(ref.AffectsPath(Path())); + EXPECT_FALSE(ref.AffectsPath(Path("foo"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo")); + + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo/bar/baz")); + + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo")); + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Keep(Path("foo/bar/baz")); + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_FALSE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } + { + PruneForest forest; + PruneForestRef ref(&forest); + ref.Prune(Path("foo")); + ref.Keep(Path("foo/bar/baz")); + EXPECT_TRUE(ref.AffectsPath(Path())); + EXPECT_TRUE(ref.AffectsPath(Path("foo"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/baz/quux"))); + + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz"))); + EXPECT_TRUE(ref.AffectsPath(Path("foo/bar/buzz/quuz"))); + } +} + +TEST(PruneForestTest, GetChild) { + PruneForest forest; + PruneForestRef ref(&forest); + + forest.SetValueAt(Path("aaa"), true); + forest.SetValueAt(Path("aaa/bbb"), true); + forest.SetValueAt(Path("aaa/bbb/ccc"), true); + forest.SetValueAt(Path("zzz"), false); + forest.SetValueAt(Path("zzz/yyy"), false); + forest.SetValueAt(Path("zzz/yyy/xxx"), false); + + PruneForest* child_aaa = forest.GetChild(Path("aaa")); + PruneForest* child_aaa_bbb = forest.GetChild(Path("aaa/bbb")); + PruneForest* child_aaa_bbb_ccc = forest.GetChild(Path("aaa/bbb/ccc")); + PruneForest* child_zzz = forest.GetChild(Path("zzz")); + PruneForest* child_zzz_yyy = forest.GetChild(Path("zzz/yyy")); + PruneForest* child_zzz_yyy_xxx = forest.GetChild(Path("zzz/yyy/xxx")); + + EXPECT_EQ(ref.GetChild(Path("aaa")), PruneForestRef(child_aaa)); + EXPECT_EQ(ref.GetChild(Path("aaa/bbb")), PruneForestRef(child_aaa_bbb)); + EXPECT_EQ(ref.GetChild(Path("aaa/bbb/ccc")), + PruneForestRef(child_aaa_bbb_ccc)); + EXPECT_EQ(ref.GetChild(Path("zzz")), PruneForestRef(child_zzz)); + EXPECT_EQ(ref.GetChild(Path("zzz/yyy")), PruneForestRef(child_zzz_yyy)); + EXPECT_EQ(ref.GetChild(Path("zzz/yyy/xxx")), + PruneForestRef(child_zzz_yyy_xxx)); +} + +TEST(PruneForestTest, Prune) { + PruneForest forest; + PruneForestRef ref(&forest); + + ref.Prune(Path("aaa/bbb/ccc")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/bbb/ccc"))); + + ref.Prune(Path("aaa/bbb")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/bbb"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Prune(Path("aaa")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Prune(Path()); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Prune(Path("zzz")); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + + ref.Prune(Path("zzz/yyy")); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + + ref.Prune(Path("zzz/yyy/xxx")); + EXPECT_TRUE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy/xxx")), nullptr); +} + +TEST(PruneForestTest, Keep) { + PruneForest forest; + PruneForestRef ref(&forest); + + ref.Keep(Path("aaa/bbb/ccc")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/bbb/ccc"))); + + ref.Keep(Path("aaa/bbb")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/bbb"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Keep(Path("aaa")); + EXPECT_EQ(forest.GetValueAt(Path()), nullptr); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa"))); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Keep(Path()); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("aaa")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/bbb/ccc")), nullptr); + + ref.Keep(Path("zzz")); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + + ref.Keep(Path("zzz/yyy")); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + + ref.Keep(Path("zzz/yyy/xxx")); + EXPECT_FALSE(*forest.GetValueAt(Path())); + EXPECT_EQ(forest.GetValueAt(Path("zzz")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("zzz/yyy/xxx")), nullptr); +} + +TEST(PruneForestTest, KeepAll) { + // Set up a test case. + PruneForest default_forest; + default_forest.SetValueAt(Path("aaa/111"), true); + default_forest.SetValueAt(Path("aaa/222"), false); + + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path("aaa"), std::set({std::string("111")})); + + // Only 111 should be affected, and it should now be false. + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path("aaa"), std::set({std::string("222")})); + + // Only 222 should be affected, but it was already false so it should not + // change. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path("aaa"), std::set( + {std::string("111"), std::string("222")})); + + // Both children should now be false. + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.KeepAll(Path(), std::set({std::string("aaa")})); + + // aaa should now be false, and all children of it should be eliminated. + EXPECT_FALSE(*forest.GetValueAt(Path("aaa"))); + + // Children are now eliminated. + EXPECT_EQ(forest.GetValueAt(Path("aaa/111")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/222")), nullptr); + } +} + +TEST(PruneForestTest, PruneAll) { + // Set up a test case. + PruneForest default_forest; + default_forest.SetValueAt(Path("aaa/111"), true); + default_forest.SetValueAt(Path("aaa/222"), false); + default_forest.SetValueAt(Path("bbb/111"), true); + default_forest.SetValueAt(Path("bbb/222"), false); + + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path("aaa"), std::set({std::string("111")})); + + // Only 111 should be affected, but it was already true so it should not + // change. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("aaa/222"))); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path("aaa"), std::set({std::string("222")})); + + // Only 222 should be affected, and it should now be true + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/222"))); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path("aaa"), std::set( + {std::string("111"), std::string("222")})); + + // Both children should now be true. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/111"))); + EXPECT_TRUE(*forest.GetValueAt(Path("aaa/222"))); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } + { + PruneForest forest = default_forest; + PruneForestRef ref(&forest); + + ref.PruneAll(Path(), std::set({std::string("aaa")})); + + // aaa should now be true, and all children of it should be eliminated. + EXPECT_TRUE(*forest.GetValueAt(Path("aaa"))); + + // Children are now eliminated. + EXPECT_EQ(forest.GetValueAt(Path("aaa/111")), nullptr); + EXPECT_EQ(forest.GetValueAt(Path("aaa/222")), nullptr); + + // Should remain untouched. + EXPECT_TRUE(*forest.GetValueAt(Path("bbb/111"))); + EXPECT_FALSE(*forest.GetValueAt(Path("bbb/222"))); + } +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/push_child_name_generator_test.cc b/database/tests/desktop/push_child_name_generator_test.cc new file mode 100644 index 0000000000..737d08e12f --- /dev/null +++ b/database/tests/desktop/push_child_name_generator_test.cc @@ -0,0 +1,87 @@ +// Copyright 2017 Google LLC +// +// 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 "database/src/desktop/push_child_name_generator.h" + +#include +#include +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "thread/fiber/fiber.h" + +namespace { + +using ::firebase::database::internal::PushChildNameGenerator; +using ::testing::Eq; +using ::testing::Lt; + +TEST(PushChildNameGeneratorTest, TestOrderOfGeneratedNamesSameTime) { + PushChildNameGenerator generator; + + // Names should be generated in a way such that they are lexicographically + // increasing. + std::vector keys; + keys.reserve(100); + for (int i = 0; i < 100; ++i) { + keys.push_back(generator.GeneratePushChildName(0)); + } + for (int i = 0; i < 99; ++i) { + EXPECT_THAT(keys[i], Lt(keys[i + 1])); + } +} + +TEST(PushChildNameGeneratorTest, TestOrderOfGeneratedNamesDifferentTime) { + PushChildNameGenerator generator; + const int kNumToTest = 100; + + // Names should be generated in a way such that they are lexicographically + // increasing. + std::vector keys; + keys.reserve(kNumToTest); + for (int i = 0; i < kNumToTest; ++i) { + keys.push_back(generator.GeneratePushChildName(i)); + } + for (int i = 0; i < kNumToTest - 1; ++i) { + EXPECT_THAT(keys[i], Lt(keys[i + 1])); + } +} + +TEST(PushChildNameGeneratorTest, TestSimultaneousGeneratedNames) { + PushChildNameGenerator generator; + const int kNumToTest = 100; + + // Create a bunch of keys. + std::vector keys; + keys.resize(kNumToTest); + std::vector fibers; + for (int i = 0; i < kNumToTest; i++) { + fibers.push_back(new thread::Fiber([&generator, &keys, i]() { + keys[i] = generator.GeneratePushChildName(std::time(nullptr)); + })); + } + + // Insert keys into set. If there is a duplicate key, it will be discarded. + std::set key_set; + for (int i = 0; i < kNumToTest; i++) { + fibers[i]->Join(); + key_set.insert(keys[i]); + delete fibers[i]; + fibers[i] = nullptr; + } + + // Ensure that all keys are unique by making sure no keys were discarded. + EXPECT_THAT(key_set.size(), Eq(kNumToTest)); +} + +} // namespace diff --git a/database/tests/desktop/test/matchers.h b/database/tests/desktop/test/matchers.h new file mode 100644 index 0000000000..b6fc84fb82 --- /dev/null +++ b/database/tests/desktop/test/matchers.h @@ -0,0 +1,40 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MATCHERS_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MATCHERS_H_ + +#include +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +// Check a smart pointer with a raw pointer for equality. Ideally we would just +// do: +// +// Pointwise(Property(&UniquePtr::get, Eq())), +// +// but Property can't handle tuple matchers. +MATCHER(SmartPtrRawPtrEq, "CheckSmartPtrRawPtrEq") { + return std::get<0>(arg).get() == std::get<1>(arg); +} + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MATCHERS_H_ diff --git a/database/tests/desktop/test/matchers_test.cc b/database/tests/desktop/test/matchers_test.cc new file mode 100644 index 0000000000..1bc44bcf29 --- /dev/null +++ b/database/tests/desktop/test/matchers_test.cc @@ -0,0 +1,73 @@ +// Copyright 2018 Google LLC +// +// 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 "database/tests/desktop/test/matchers.h" + +#include + +#include "app/memory/unique_ptr.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Not; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { + +TEST(SmartPtrRawPtrEq, Matcher) { + int* five = new int(5); + EXPECT_THAT(std::make_tuple(UniquePtr(five), five), SmartPtrRawPtrEq()); + + int* ten = new int(10); + int* different_ten = new int(10); + EXPECT_THAT(std::make_tuple(UniquePtr(ten), different_ten), + Not(SmartPtrRawPtrEq())); + delete different_ten; +} + +TEST(SmartPtrRawPtrEq, Pointwise) { + int* five = new int(5); + int* ten = new int(10); + int* fifteen = new int(15); + int* twenty = new int(20); + int* different_twenty = new int(20); + std::vector> unique_values{ + UniquePtr(five), + UniquePtr(ten), + UniquePtr(fifteen), + UniquePtr(twenty), + }; + std::vector raw_values{ + five, + ten, + fifteen, + twenty, + }; + std::vector wrong_raw_values{ + five, + ten, + fifteen, + different_twenty, + }; + EXPECT_THAT(unique_values, Pointwise(SmartPtrRawPtrEq(), raw_values)); + EXPECT_THAT(unique_values, + Not(Pointwise(SmartPtrRawPtrEq(), wrong_raw_values))); + delete different_twenty; +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/test/mock_cache_policy.h b/database/tests/desktop/test/mock_cache_policy.h new file mode 100644 index 0000000000..ae61bb7413 --- /dev/null +++ b/database/tests/desktop/test/mock_cache_policy.h @@ -0,0 +1,45 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_CACHE_POLICY_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_CACHE_POLICY_H_ + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/cache_policy.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockCachePolicy : public CachePolicy { + public: + ~MockCachePolicy() override {} + + MOCK_METHOD(bool, ShouldPrune, + (uint64_t current_size_bytes, uint64_t count_of_prunable_queries), + (const, override)); + MOCK_METHOD(bool, ShouldCheckCacheSize, + (uint64_t server_updates_since_last_check), (const, override)); + MOCK_METHOD(double, GetPercentOfQueriesToPruneAtOnce, (), (const, override)); + MOCK_METHOD(uint64_t, GetMaxNumberOfQueriesToKeep, (), (const, override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_CACHE_POLICY_H_ diff --git a/database/tests/desktop/test/mock_listen_provider.h b/database/tests/desktop/test/mock_listen_provider.h new file mode 100644 index 0000000000..67d83ca796 --- /dev/null +++ b/database/tests/desktop/test/mock_listen_provider.h @@ -0,0 +1,40 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTEN_PROVIDER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTEN_PROVIDER_H_ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/listen_provider.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockListenProvider : public ListenProvider { + public: + MOCK_METHOD(void, StartListening, + (const QuerySpec& query_spec, const Tag& tag, const View* view), + (override)); + MOCK_METHOD(void, StopListening, + (const QuerySpec& query_spec, const Tag& tag), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTEN_PROVIDER_H_ diff --git a/database/tests/desktop/test/mock_listener.h b/database/tests/desktop/test/mock_listener.h new file mode 100644 index 0000000000..f612e9bcc8 --- /dev/null +++ b/database/tests/desktop/test/mock_listener.h @@ -0,0 +1,55 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTENER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTENER_H_ + +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/include/firebase/database/common.h" +#include "database/src/include/firebase/database/listener.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockValueListener : public ValueListener { + public: + MOCK_METHOD(void, OnValueChanged, (const DataSnapshot& snapshot), (override)); + MOCK_METHOD(void, OnCancelled, + (const Error& error, const char* error_message), (override)); +}; + +class MockChildListener : public ChildListener { + public: + MOCK_METHOD(void, OnChildAdded, + (const DataSnapshot& snapshot, const char* previous_sibling_key), + (override)); + MOCK_METHOD(void, OnChildChanged, + (const DataSnapshot& snapshot, const char* previous_sibling_key), + (override)); + MOCK_METHOD(void, OnChildMoved, + (const DataSnapshot& snapshot, const char* previous_sibling_key), + (override)); + MOCK_METHOD(void, OnChildRemoved, (const DataSnapshot& snapshot), (override)); + MOCK_METHOD(void, OnCancelled, + (const Error& error, const char* error_message), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_LISTENER_H_ diff --git a/database/tests/desktop/test/mock_persistence_manager.h b/database/tests/desktop/test/mock_persistence_manager.h new file mode 100644 index 0000000000..774be6a488 --- /dev/null +++ b/database/tests/desktop/test/mock_persistence_manager.h @@ -0,0 +1,77 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_MANAGER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_MANAGER_H_ + +#include "app/src/include/firebase/variant.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/cache_policy.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/src/desktop/persistence/persistence_manager.h" +#include "database/src/desktop/view/view_cache.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockPersistenceManager : public PersistenceManager { + public: + MockPersistenceManager( + UniquePtr storage_engine, + UniquePtr tracked_query_manager, + UniquePtr cache_policy, LoggerBase* logger) + : PersistenceManager(std::move(storage_engine), + std::move(tracked_query_manager), + std::move(cache_policy), logger) {} + ~MockPersistenceManager() override {} + + MOCK_METHOD(void, SaveUserOverwrite, + (const Path& path, const Variant& variant, WriteId write_id), + (override)); + MOCK_METHOD(void, SaveUserMerge, + (const Path& path, const CompoundWrite& children, + WriteId write_id), + (override)); + MOCK_METHOD(void, RemoveUserWrite, (WriteId write_id), (override)); + MOCK_METHOD(void, RemoveAllUserWrites, (), (override)); + MOCK_METHOD(void, ApplyUserWriteToServerCache, + (const Path& path, const Variant& variant), (override)); + MOCK_METHOD(void, ApplyUserWriteToServerCache, + (const Path& path, const CompoundWrite& merge), (override)); + MOCK_METHOD(std::vector, LoadUserWrites, (), (override)); + MOCK_METHOD(CacheNode, ServerCache, (const QuerySpec& query), (override)); + MOCK_METHOD(void, UpdateServerCache, + (const QuerySpec& query, const Variant& variant), (override)); + MOCK_METHOD(void, UpdateServerCache, + (const Path& path, const CompoundWrite& children), (override)); + MOCK_METHOD(void, SetQueryActive, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetQueryInactive, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetQueryComplete, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetTrackedQueryKeys, + (const QuerySpec& query, const std::set& keys), + (override)); + MOCK_METHOD(void, UpdateTrackedQueryKeys, + (const QuerySpec& query, const std::set& added, + const std::set& removed), + (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_MANAGER_H_ diff --git a/database/tests/desktop/test/mock_persistence_storage_engine.h b/database/tests/desktop/test/mock_persistence_storage_engine.h new file mode 100644 index 0000000000..7db6674127 --- /dev/null +++ b/database/tests/desktop/test/mock_persistence_storage_engine.h @@ -0,0 +1,79 @@ +// Copyright 2018 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_STORAGE_ENGINE_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_STORAGE_ENGINE_H_ + +#include + +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/compound_write.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockPersistenceStorageEngine : public PersistenceStorageEngine { + public: + MOCK_METHOD(void, SaveUserOverwrite, + (const Path& path, const Variant& data, WriteId write_id), + (override)); + MOCK_METHOD(void, SaveUserMerge, + (const Path& path, const CompoundWrite& children, + WriteId write_id), + (override)); + MOCK_METHOD(void, RemoveUserWrite, (WriteId write_id), (override)); + MOCK_METHOD(std::vector, LoadUserWrites, (), (override)); + MOCK_METHOD(void, RemoveAllUserWrites, (), (override)); + MOCK_METHOD(Variant, ServerCache, (const Path& path), (override)); + MOCK_METHOD(void, OverwriteServerCache, + (const Path& path, const Variant& data), (override)); + MOCK_METHOD(void, MergeIntoServerCache, + (const Path& path, const Variant& data), (override)); + MOCK_METHOD(void, MergeIntoServerCache, + (const Path& path, const CompoundWrite& children), (override)); + MOCK_METHOD(uint64_t, ServerCacheEstimatedSizeInBytes, (), (const, override)); + MOCK_METHOD(void, SaveTrackedQuery, (const TrackedQuery& tracked_query), + (override)); + MOCK_METHOD(void, DeleteTrackedQuery, (QueryId tracked_query_id), (override)); + MOCK_METHOD(std::vector, LoadTrackedQueries, (), (override)); + MOCK_METHOD(void, ResetPreviouslyActiveTrackedQueries, (uint64_t last_use), + (override)); + MOCK_METHOD(void, SaveTrackedQueryKeys, + (QueryId tracked_query_id, const std::set& keys), + (override)); + MOCK_METHOD(void, UpdateTrackedQueryKeys, + (QueryId tracked_query_id, const std::set& added, + const std::set& removed), + (override)); + MOCK_METHOD(std::set, LoadTrackedQueryKeys, + (QueryId tracked_query_id), (override)); + MOCK_METHOD(std::set, LoadTrackedQueryKeys, + (const std::set& trackedQueryIds), (override)); + MOCK_METHOD(void, PruneCache, + (const Path& root, const PruneForestRef& prune_forest), + (override)); + MOCK_METHOD(bool, BeginTransaction, (), (override)); + MOCK_METHOD(void, EndTransaction, (), (override)); + MOCK_METHOD(void, SetTransactionSuccessful, (), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_PERSISTENCE_STORAGE_ENGINE_H_ diff --git a/database/tests/desktop/test/mock_tracked_query_manager.h b/database/tests/desktop/test/mock_tracked_query_manager.h new file mode 100644 index 0000000000..0c5b0ccdab --- /dev/null +++ b/database/tests/desktop/test/mock_tracked_query_manager.h @@ -0,0 +1,52 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_TRACKED_QUERY_MANAGER_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_TRACKED_QUERY_MANAGER_H_ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/tracked_query_manager.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockTrackedQueryManager : public TrackedQueryManagerInterface { + public: + MOCK_METHOD(const TrackedQuery*, FindTrackedQuery, (const QuerySpec& query), + (const, override)); + MOCK_METHOD(void, RemoveTrackedQuery, (const QuerySpec& query), (override)); + MOCK_METHOD(void, SetQueryActiveFlag, + (const QuerySpec& query, + TrackedQuery::ActivityStatus activity_status), + (override)); + MOCK_METHOD(void, SetQueryCompleteIfExists, (const QuerySpec& query), + (override)); + MOCK_METHOD(void, SetQueriesComplete, (const Path& path), (override)); + MOCK_METHOD(bool, IsQueryComplete, (const QuerySpec& query), (override)); + MOCK_METHOD(PruneForest, PruneOldQueries, (const CachePolicy& cache_policy), + (override)); + MOCK_METHOD(std::set, GetKnownCompleteChildren, + (const Path& path), (override)); + MOCK_METHOD(void, EnsureCompleteTrackedQuery, (const Path& path), (override)); + MOCK_METHOD(bool, HasActiveDefaultQuery, (const Path& path), (override)); + MOCK_METHOD(uint64_t, CountOfPrunableQueries, (), (override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_TRACKED_QUERY_MANAGER_H_ diff --git a/database/tests/desktop/test/mock_write_tree.h b/database/tests/desktop/test/mock_write_tree.h new file mode 100644 index 0000000000..5224ce0a7b --- /dev/null +++ b/database/tests/desktop/test/mock_write_tree.h @@ -0,0 +1,80 @@ +// Copyright 2019 Google LLC +// +// 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. + +#ifndef FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_WRITE_TREE_H_ +#define FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_WRITE_TREE_H_ + +#include +#include +#include "app/src/include/firebase/variant.h" +#include "app/src/optional.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/write_tree.h" +#include "database/src/desktop/persistence/persistence_storage_engine.h" +#include "database/src/desktop/view/view_cache.h" + +namespace firebase { +namespace database { +namespace internal { + +class MockWriteTree : public WriteTree { + public: + MOCK_METHOD(Optional, CalcCompleteEventCache, + (const Path& tree_path, const Variant* complete_server_cache), + (const, override)); + + MOCK_METHOD(Optional, CalcCompleteEventCache, + (const Path& tree_path, const Variant* complete_server_cache, + const std::vector& write_ids_to_exclude), + (const, override)); + + MOCK_METHOD(Optional, CalcCompleteEventCache, + (const Path& tree_path, const Variant* complete_server_cache, + const std::vector& write_ids_to_exclude, + HiddenWriteInclusion include_hidden_writes), + (const, override)); + + MOCK_METHOD(Variant, CalcCompleteEventChildren, + (const Path& tree_path, const Variant& complete_server_children), + (const, override)); + + MOCK_METHOD(Optional, CalcEventCacheAfterServerOverwrite, + (const Path& tree_path, const Path& path, + const Variant* existing_local_snap, + const Variant* existing_server_snap), + (const, override)); + + MOCK_METHOD((Optional>), CalcNextVariantAfterPost, + (const Path& tree_path, + const Optional& complete_server_data, + (const std::pair& post), + IterationDirection direction, const QueryParams& params), + (const, override)); + + MOCK_METHOD(Optional, ShadowingWrite, (const Path& path), + (const, override)); + + MOCK_METHOD(Optional, CalcCompleteChild, + (const Path& tree_path, const std::string& child_key, + const CacheNode& existing_server_cache), + (const, override)); +}; + +} // namespace internal +} // namespace database +} // namespace firebase + +#endif // FIREBASE_DATABASE_CLIENT_CPP_TESTS_DESKTOP_TEST_MOCK_WRITE_TREE_H_ diff --git a/database/tests/desktop/util_desktop_test.cc b/database/tests/desktop/util_desktop_test.cc new file mode 100644 index 0000000000..70a86c65c8 --- /dev/null +++ b/database/tests/desktop/util_desktop_test.cc @@ -0,0 +1,2775 @@ +// Copyright 2017 Google LLC +// +// 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 "database/src/desktop/util_desktop.h" + +#include +#include + +#include +#include +#include +#if defined(_WIN32) +#include +static const char* kPathSep = "\\"; +#define unlink _unlink +#else +static const char* kPathSep = "//"; +#endif + +#include "app/src/include/firebase/variant.h" +#include "app/src/path.h" +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +using ::testing::Eq; +using ::testing::Pair; +using ::testing::Property; +using ::testing::StrEq; +using ::testing::UnorderedElementsAre; + +TEST(UtilDesktopTest, IsPriorityKey) { + EXPECT_FALSE(IsPriorityKey("")); + EXPECT_FALSE(IsPriorityKey("A")); + EXPECT_FALSE(IsPriorityKey(".priority_queue")); + EXPECT_FALSE(IsPriorityKey(".priority ")); + EXPECT_FALSE(IsPriorityKey(" .priority")); + EXPECT_TRUE(IsPriorityKey(".priority")); +} + +TEST(UtilDesktopTest, StringStartsWith) { + EXPECT_TRUE(StringStartsWith("abcde", "")); + EXPECT_TRUE(StringStartsWith("abcde", "abc")); + EXPECT_TRUE(StringStartsWith("abcde", "abcde")); + + EXPECT_FALSE(StringStartsWith("abcde", "zzzzz")); + EXPECT_FALSE(StringStartsWith("abcde", "aaaaa")); + EXPECT_FALSE(StringStartsWith("abcde", "cde")); + EXPECT_FALSE(StringStartsWith("abcde", "abcdefghijklmnopqrstuvwxyz")); +} + +TEST(UtilDesktopTest, MapGet) { + std::map string_map{ + std::make_pair("one", 1), + std::make_pair("two", 2), + std::make_pair("three", 3), + }; + + // Get a value that does exist, non-const. + EXPECT_EQ(*MapGet(&string_map, "one"), 1); + EXPECT_EQ(*MapGet(&string_map, std::string("one")), 1); + // Get a value that does not exist, non-const. + EXPECT_EQ(MapGet(&string_map, "zero"), nullptr); + EXPECT_EQ(MapGet(&string_map, std::string("zero")), nullptr); + // Get a value that does exist, const. + EXPECT_EQ(*MapGet(&string_map, "two"), 2); + EXPECT_EQ(*MapGet(&string_map, std::string("two")), 2); + // Get a value that does not exist, const. + EXPECT_EQ(MapGet(&string_map, "zero"), nullptr); + EXPECT_EQ(MapGet(&string_map, std::string("zero")), nullptr); +} + +TEST(UtilDesktopTest, Extend) { + { + std::vector a{1, 2, 3, 4}; + std::vector b{5, 6, 7, 8}; + + Extend(&a, b); + EXPECT_EQ(a, (std::vector{1, 2, 3, 4, 5, 6, 7, 8})); + } + { + std::vector a; + std::vector b{5, 6, 7, 8}; + + Extend(&a, b); + EXPECT_EQ(a, (std::vector{5, 6, 7, 8})); + } + { + std::vector a{1, 2, 3, 4}; + std::vector b; + + Extend(&a, b); + EXPECT_EQ(a, (std::vector{1, 2, 3, 4})); + } +} + +TEST(UtilDesktopTest, PatchVariant) { + std::map starting_map{ + std::make_pair("a", 1), + std::make_pair("b", 2), + std::make_pair("c", 3), + }; + + // Completely overlapping data. + { + std::map patch_map{ + std::make_pair("a", 10), + std::make_pair("b", 20), + std::make_pair("c", 30), + }; + Variant data(starting_map); + Variant patch_data(patch_map); + EXPECT_TRUE(PatchVariant(patch_data, &data)); + EXPECT_TRUE(data.is_map()); + EXPECT_THAT(data.map(), UnorderedElementsAre(Pair(Eq("a"), Eq(10)), + Pair(Eq("b"), Eq(20)), + Pair(Eq("c"), Eq(30)))); + } + + // Completely disjoint data. + { + std::map patch_map{ + std::make_pair("d", 40), + std::make_pair("e", 50), + std::make_pair("f", 60), + }; + Variant data(starting_map); + Variant patch_data(patch_map); + EXPECT_TRUE(PatchVariant(patch_data, &data)); + EXPECT_TRUE(data.is_map()); + EXPECT_THAT(data.map(), UnorderedElementsAre( + Pair(Eq("a"), Eq(1)), Pair(Eq("b"), Eq(2)), + Pair(Eq("c"), Eq(3)), Pair(Eq("d"), Eq(40)), + Pair(Eq("e"), Eq(50)), Pair(Eq("f"), Eq(60)))); + } + + // Partially overlapping data. + { + std::map patch_map{ + std::make_pair("a", 100), + std::make_pair("d", 400), + std::make_pair("f", 600), + }; + Variant data(starting_map); + Variant patch_data(patch_map); + EXPECT_TRUE(PatchVariant(patch_data, &data)); + EXPECT_TRUE(data.is_map()); + EXPECT_THAT(data.map(), UnorderedElementsAre( + Pair(Eq("a"), Eq(100)), Pair(Eq("b"), Eq(2)), + Pair(Eq("c"), Eq(3)), Pair(Eq("d"), Eq(400)), + Pair(Eq("f"), Eq(600)))); + } + + // Source data is not a map. + { + Variant data; + std::map patch_map{ + std::make_pair("a", 10), + std::make_pair("b", 20), + std::make_pair("c", 30), + }; + Variant patch_data(patch_map); + EXPECT_FALSE(PatchVariant(patch_data, &data)); + } + // Patch data is not a map. + { + Variant data(starting_map); + Variant patch_data; + + EXPECT_FALSE(PatchVariant(patch_data, &data)); + } + + // Neither source nor patch data is a map. + { + Variant data; + Variant patch_data; + EXPECT_FALSE(PatchVariant(patch_data, &data)); + } +} + +TEST(UtilDesktopTest, VariantGetChild) { + Variant null_variant; + EXPECT_EQ(VariantGetChild(&null_variant, Path()), Variant::Null()); + EXPECT_EQ(VariantGetChild(&null_variant, Path("aaa")), Variant::Null()); + EXPECT_EQ(VariantGetChild(&null_variant, Path("aaa/bbb")), Variant::Null()); + + Variant leaf_variant = 100; + EXPECT_EQ(VariantGetChild(&leaf_variant, Path()), 100); + EXPECT_EQ(VariantGetChild(&leaf_variant, Path("aaa")), Variant::Null()); + EXPECT_EQ(VariantGetChild(&leaf_variant, Path("aaa/bbb")), Variant::Null()); + + Variant prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, Path()), + Variant(std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + })); + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, Path("aaa")), + Variant::Null()); + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, Path("aaa/bbb")), + Variant::Null()); + + Variant map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + }), + }); + + EXPECT_EQ(VariantGetChild(&map_variant, Path()), map_variant); + EXPECT_EQ(VariantGetChild(&map_variant, Path("aaa")), 100); + EXPECT_EQ(VariantGetChild(&map_variant, Path("aaa/bbb")), Variant::Null()); + EXPECT_EQ(VariantGetChild(&map_variant, Path("bbb")), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + })); + EXPECT_EQ(VariantGetChild(&map_variant, Path("bbb/ccc")), Variant(200)); + + Variant prioritized_map_variant(std::map{ + std::make_pair(".priority", 1), + std::make_pair("aaa", + std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + }), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + std::make_pair(".priority", 3), + }), + }); + + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path()), + prioritized_map_variant); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("aaa")), + Variant(std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + })); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("aaa/bbb")), + Variant::Null()); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("bbb")), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + })); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, Path("bbb/ccc")), + Variant(200)); +} + +TEST(UtilDesktopTest, VariantGetImmediateChild) { + Variant null_variant; + EXPECT_EQ(VariantGetChild(&null_variant, "aaa"), Variant::Null()); + EXPECT_EQ(VariantGetChild(&null_variant, ".priority"), Variant::Null()); + + Variant leaf_variant = 100; + EXPECT_EQ(VariantGetChild(&leaf_variant, "aaa"), Variant::Null()); + + Variant prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + EXPECT_EQ(VariantGetChild(&prioritized_leaf_variant, "aaa"), Variant::Null()); + + Variant map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + }), + }); + + EXPECT_EQ(VariantGetChild(&map_variant, "aaa"), 100); + EXPECT_EQ(VariantGetChild(&map_variant, "bbb"), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + })); + + Variant prioritized_map_variant(std::map{ + std::make_pair(".priority", 1), + std::make_pair("aaa", + std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + }), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + }), + std::make_pair("fff", + std::map{ + std::make_pair("ggg", 500), + std::make_pair("hhh", 600), + std::make_pair("iii", 700), + std::make_pair(".priority", 3), + }), + }); + + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, "aaa"), + Variant(std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", 1), + })); + EXPECT_EQ(VariantGetChild(&prioritized_map_variant, "bbb"), + Variant(std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + std::make_pair(".priority", 2), + })); +} + +TEST(UtilDesktopTest, VariantUpdateChild_NullVariant) { + Variant null_variant; + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(), 100); + EXPECT_EQ(null_variant, Variant(100)); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/bbb"), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::EmptyMap()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/bbb"), Variant::EmptyMap()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(".priority"), 100); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/.priority"), 100); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), 100); + EXPECT_EQ(null_variant, + Variant(std::map{std::make_pair("aaa", 100)})); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa/bbb"), 1234); + EXPECT_EQ(null_variant, Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", 1234), + }), + })); +} + +TEST(UtilDesktopTest, VariantUpdateChild_LeafVariant) { + Variant leaf_variant = 100; + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path(), Variant::Null()); + EXPECT_EQ(leaf_variant, Variant::Null()); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path(), Variant(1234)); + EXPECT_EQ(leaf_variant, Variant(1234)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa/bbb"), Variant::Null()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa/bbb"), Variant::EmptyMap()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path(".priority"), 999); + EXPECT_EQ(leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa"), 1234); + EXPECT_EQ(leaf_variant, + Variant(std::map{std::make_pair("aaa", 1234)})); + leaf_variant = 100; + VariantUpdateChild(&leaf_variant, Path("aaa/bbb"), 1234); + EXPECT_EQ(leaf_variant, Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", 1234), + }), + })); + + const Variant original_prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + Variant prioritized_leaf_variant; + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path(), Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, Variant::Null()); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path(), Variant(1234)); + EXPECT_EQ(prioritized_leaf_variant, Variant(1234)); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa/bbb"), + Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa/bbb"), + Variant::EmptyMap()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path(".priority"), 999); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa"), 1234); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 10), + std::make_pair("aaa", 1234), + })); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, Path("aaa/bbb"), 1234); + EXPECT_EQ(prioritized_leaf_variant, + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", 1234), + }), + std::make_pair(".priority", 10), + })); +} + +TEST(UtilDesktopTest, VariantUpdateChild_MapVariant) { + Variant original_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }); + Variant map_variant = original_map_variant; + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(), Variant::Null()); + EXPECT_EQ(map_variant, Variant::Null()); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(), Variant(9999)); + EXPECT_EQ(map_variant, Variant(9999)); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(".priority"), Variant::Null()); + EXPECT_EQ(map_variant, original_map_variant); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path(".priority"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("aaa"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("bbb"), Variant::Null()); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("bbb"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("ccc"), Variant::Null()); + EXPECT_EQ(map_variant, original_map_variant); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, Path("ccc"), Variant(9999)); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + })); + + Variant original_prioritized_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + }); + Variant prioritized_map_variant; + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, Variant::Null()); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, Variant(9999)); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(".priority"), + Variant::Null()); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path(".priority"), + Variant(9999)); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("aaa"), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("bbb"), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("bbb"), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("ccc"), Variant::Null()); + EXPECT_EQ(prioritized_map_variant, original_prioritized_map_variant); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, Path("ccc"), Variant(9999)); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + std::make_pair(".priority", 1234), + })); +} + +TEST(UtilDesktopTest, VariantUpdateImmediateChild_NullVariant) { + Variant null_variant; + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::Null()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), Variant::EmptyMap()); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path(".priority"), 100); + EXPECT_EQ(null_variant, Variant::Null()); + + null_variant = Variant::Null(); + VariantUpdateChild(&null_variant, Path("aaa"), 100); + EXPECT_EQ(null_variant, + Variant(std::map{std::make_pair("aaa", 100)})); +} + +TEST(UtilDesktopTest, VariantUpdateImmediateChild_LeafVariant) { + const Variant original_leaf_variant = 100; + Variant leaf_variant; + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, "aaa", Variant::Null()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, "aaa", Variant::EmptyMap()); + EXPECT_EQ(leaf_variant, Variant(100)); + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, ".priority", 999); + EXPECT_EQ(leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + + leaf_variant = original_leaf_variant; + VariantUpdateChild(&leaf_variant, "aaa", 1234); + EXPECT_EQ(leaf_variant, + Variant(std::map{std::make_pair("aaa", 1234)})); + + const Variant original_prioritized_leaf_variant = std::map{ + std::make_pair(".priority", 10), + std::make_pair(".value", 100), + }; + Variant prioritized_leaf_variant; + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, "aaa", Variant::Null()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, "aaa", Variant::EmptyMap()); + EXPECT_EQ(prioritized_leaf_variant, original_prioritized_leaf_variant); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, ".priority", 999); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 999), + std::make_pair(".value", 100), + })); + + prioritized_leaf_variant = original_prioritized_leaf_variant; + VariantUpdateChild(&prioritized_leaf_variant, "aaa", 1234); + EXPECT_EQ(prioritized_leaf_variant, Variant(std::map{ + std::make_pair(".priority", 10), + std::make_pair("aaa", 1234), + })); +} + +TEST(UtilDesktopTest, VariantUpdateImmediateChild_MapVariant) { + Variant original_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }); + Variant map_variant; + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, ".priority", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, "aaa", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, "bbb", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + })); + + map_variant = original_map_variant; + VariantUpdateChild(&map_variant, "ccc", 9999); + EXPECT_EQ(map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + })); + + Variant original_prioritized_map_variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + }); + Variant prioritized_map_variant; + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, ".priority", 9999); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 9999), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, "aaa", 9999); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 9999), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, "bbb", 9999); + EXPECT_EQ(prioritized_map_variant, Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 9999), + std::make_pair(".priority", 1234), + })); + + prioritized_map_variant = original_prioritized_map_variant; + VariantUpdateChild(&prioritized_map_variant, "ccc", 9999); + EXPECT_EQ(prioritized_map_variant, + Variant(std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + std::make_pair("ccc", 9999), + std::make_pair(".priority", 1234), + })); +} + +TEST(UtilDesktopTest, GetVariantAtPath) { + std::map candy{}; + std::map fruits{ + std::make_pair("apple", "red"), + std::make_pair("banana", "yellow"), + std::make_pair("grape", "purple"), + }; + std::map vegetables{ + std::make_pair(".value", std::map{ + std::make_pair("broccoli", "green"), + std::make_pair("carrot", "orange"), + std::make_pair("cauliflower", "white"), + })}; + std::map healthy_food_map{ + std::make_pair("candy", candy), + std::make_pair("fruits", fruits), + std::make_pair("vegetables", vegetables), + }; + + // Get root value. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, Path::GetRoot()); + EXPECT_EQ(result, &healthy_food); + } + + // Get valid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, Path("fruits")); + EXPECT_EQ(result, &healthy_food.map()["fruits"]); + } + + // Get valid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + GetInternalVariant(&healthy_food, Path("vegetables/carrot")); + EXPECT_EQ( + result, + &healthy_food.map()["vegetables"].map()[".value"].map()["carrot"]); + } + + // Get invalid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, Path("cereal")); + EXPECT_EQ(result, nullptr); + } + + // Get invalid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + GetInternalVariant(&healthy_food, Path("candy/marshmallows")); + EXPECT_EQ(result, nullptr); + } + + // Attempt to retrieve something from a non-map. + { + Variant not_a_map(100); + Variant* result = GetInternalVariant(¬_a_map, Path("fruits")); + EXPECT_EQ(result, nullptr); + } +} + +TEST(UtilDesktopTest, GetVariantAtKey) { + std::map candy{}; + std::map fruits{ + std::make_pair("apple", "red"), + std::make_pair("banana", "yellow"), + std::make_pair("grape", "purple"), + }; + std::map vegetables{ + std::make_pair("broccoli", "green"), + std::make_pair("carrot", "orange"), + std::make_pair("cauliflower", "white"), + }; + std::map healthy_food_map{ + std::make_pair(".value", + std::map{ + std::make_pair("candy", candy), + std::make_pair("fruits", fruits), + std::make_pair("vegetables", vegetables), + }), + }; + + // Get valid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, "fruits"); + EXPECT_EQ(result, &healthy_food.map()[".value"].map()["fruits"]); + } + + // Try and fail to get a grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, "vegetables/carrot"); + EXPECT_EQ(result, nullptr); + } + + // Get invalid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = GetInternalVariant(&healthy_food, "cereal"); + EXPECT_EQ(result, nullptr); + } + + // Attempt to retrieve something from a non-map. + { + Variant not_a_map(100); + Variant* result = GetInternalVariant(¬_a_map, "fruits"); + EXPECT_EQ(result, nullptr); + } +} + +TEST(UtilDesktopTest, MakeVariantAtPath) { + std::map healthy_food_map{ + std::make_pair("candy", std::map{}), + std::make_pair("fruits", + std::map{ + std::make_pair("apple", "red"), + std::make_pair("banana", "yellow"), + std::make_pair("grape", "purple"), + }), + std::make_pair("vegetables", + std::map{ + std::make_pair("broccoli", "green"), + std::make_pair("carrot", "orange"), + std::make_pair("cauliflower", + std::map{ + std::make_pair(".value", "white"), + std::make_pair(".priority", 100), + }), + }), + }; + + // Get root value. + { + Variant healthy_food(healthy_food_map); + Variant* result = MakeVariantAtPath(&healthy_food, Path::GetRoot()); + EXPECT_EQ(*result, healthy_food); + } + + // Get valid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = MakeVariantAtPath(&healthy_food, Path("fruits")); + EXPECT_EQ(result, &healthy_food.map()["fruits"]); + } + + // Get valid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("vegetables/carrot")); + EXPECT_EQ(result, &healthy_food.map()["vegetables"].map()["carrot"]); + } + + // Get invalid child. + { + Variant healthy_food(healthy_food_map); + Variant* result = MakeVariantAtPath(&healthy_food, Path("cereal")); + EXPECT_EQ(result, &healthy_food.map()["cereal"]); + EXPECT_TRUE(healthy_food.map()["candy"].is_map()); + EXPECT_EQ(healthy_food.map()["candy"].map().size(), 0); + } + + // Get invalid grandchild. + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("candy/marshmallows")); + EXPECT_NE(result, nullptr); + EXPECT_TRUE(healthy_food.is_map()); + EXPECT_TRUE(healthy_food.map()["candy"].is_map()); + EXPECT_NE(healthy_food.map()["candy"].map().find("marshmallows"), + healthy_food.map()["candy"].map().end()); + EXPECT_EQ(result, &healthy_food.map()["candy"].map()["marshmallows"]); + } + + // Attempt to retrieve something from a non-map. + { + Variant not_a_map(100); + Variant* result = MakeVariantAtPath(¬_a_map, Path("fruits")); + EXPECT_TRUE(not_a_map.is_map()); + EXPECT_EQ(result, ¬_a_map.map()["fruits"]); + EXPECT_NE(not_a_map.map().find("fruits"), not_a_map.map().end()); + } + + // Attempt to retrieve a node with a ".value". + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("vegetables/cauliflower")); + EXPECT_NE(result, nullptr); + EXPECT_EQ(*result, Variant(std::map{ + std::make_pair(".value", "white"), + std::make_pair(".priority", 100), + })); + } + + // Attempt to retrieve a node past a ".value". + { + Variant healthy_food(healthy_food_map); + Variant* result = + MakeVariantAtPath(&healthy_food, Path("vegetables/cauliflower/new")); + EXPECT_NE(result, nullptr); + EXPECT_EQ( + result, + &healthy_food.map()["vegetables"].map()["cauliflower"].map()["new"]); + EXPECT_EQ(healthy_food.map()["vegetables"] + .map()["cauliflower"] + .map()[".priority"], + 100); + EXPECT_EQ(*result, Variant::Null()); + } +} + +TEST(UtilDesktopTest, SetVariantAtPath) { + Variant initial = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 300), + std::make_pair(".priority", 999), + }), + }), + }; + + // Change existing value + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/bbb"), 1000); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 1000), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 300), + std::make_pair(".priority", 999), + }), + }), + }; + EXPECT_EQ(variant, expected); + } + + // Change existing value inside of a .value key. + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/ddd"), 3000); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 3000), + std::make_pair(".priority", 999), + }), + }), + }; + EXPECT_EQ(variant, expected); + } + + // Add a new value. + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/eee"), 4000); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair(".value", 300), + std::make_pair(".priority", 999), + }), + std::make_pair("eee", 4000), + }), + }; + EXPECT_EQ(variant, expected); + } + + // Add map at a location with a .value + { + Variant variant = initial; + SetVariantAtPath(&variant, Path("aaa/ddd"), + std::map{ + std::make_pair("zzz", 999), + std::make_pair("yyy", 888), + }); + + Variant expected = std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", 100), + std::make_pair("ccc", 200), + std::make_pair("ddd", + std::map{ + std::make_pair("zzz", 999), + std::make_pair("yyy", 888), + std::make_pair(".priority", 999), + }), + }), + }; + EXPECT_EQ(variant, expected); + } +} + +TEST(UtilDesktopTest, ParseUrlSupportCases) { + // Without Path + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com/"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test-123.firebaseio.com"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test-123.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test-123"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("http://test.firebaseio.com"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_FALSE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com"), ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com/"), ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com:80"), ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com:80"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("test.firebaseio.com:8080/"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com:8080"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, ""); + } + + // With path + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com/path/to/key"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, "path/to/key"); + } + + { + ParseUrl parse_util; + EXPECT_EQ(parse_util.Parse("https://test.firebaseio.com/path/to/key/"), + ParseUrl::kParseOk); + EXPECT_EQ(parse_util.hostname, "test.firebaseio.com"); + EXPECT_EQ(parse_util.ns, "test"); + EXPECT_TRUE(parse_util.secure); + EXPECT_EQ(parse_util.path, "path/to/key/"); + } +} + +TEST(UtilDesktopTest, ParseUrlErrorCases) { + // Test Wrong Protocol + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("://"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("://test.firebaseio.com"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("ws://test.firebaseio.com"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("ftp://test.firebaseio.com"), + ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("https:/test.firebaseio.com"), + ParseUrl::kParseOk); + } + + // Test wrong port + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:44a"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:a"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test.firebaseio.com:a43"), ParseUrl::kParseOk); + } + + // Test Wrong hostname/namespace + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse(""), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("test"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http:///"), ParseUrl::kParseOk); // NOTYPO + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://./"), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://a."), ParseUrl::kParseOk); + } + { + ParseUrl parse_util; + EXPECT_NE(parse_util.Parse("http://a....../"), ParseUrl::kParseOk); + } +} + +TEST(UtilDesktopTest, CountChildren_Fundamental_Type) { + Variant simple_value = 10; + EXPECT_EQ(CountEffectiveChildren(simple_value), 0); + + std::map children; + std::map expect_children; + EXPECT_THAT(GetEffectiveChildren(simple_value, &children), Eq(0)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, CountChildren_FundamentalTypeWithPriority) { + Variant high_priority_food = std::map{ + std::make_pair(".value", "milk chocolate"), + std::make_pair(".priority", 10000), + }; + EXPECT_EQ(CountEffectiveChildren(high_priority_food), 0); + + std::map children; + std::map expect_children; + EXPECT_THAT(GetEffectiveChildren(high_priority_food, &children), Eq(0)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, CountChildren_MapWithPriority) { + // Remove priority field. + Variant worst_foods_with_priority = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + std::make_pair(".priority", -100000), + }; + EXPECT_EQ(CountEffectiveChildren(worst_foods_with_priority), 3); + + std::map children; + std::map expect_children = { + std::make_pair("bad", &worst_foods_with_priority.map()["bad"]), + std::make_pair("badder", &worst_foods_with_priority.map()["badder"]), + std::make_pair("baddest", &worst_foods_with_priority.map()["baddest"]), + }; + EXPECT_THAT(GetEffectiveChildren(worst_foods_with_priority, &children), + Eq(3)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, CountChildren_MapWithoutPriority) { + // Remove priority field. + Variant worst_foods = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + }; + EXPECT_EQ(CountEffectiveChildren(worst_foods), 3); + + std::map children; + std::map expect_children = { + std::make_pair("bad", &worst_foods.map()["bad"]), + std::make_pair("badder", &worst_foods.map()["badder"]), + std::make_pair("baddest", &worst_foods.map()["baddest"]), + }; + EXPECT_THAT(GetEffectiveChildren(worst_foods, &children), Eq(3)); + EXPECT_EQ(children, expect_children); +} + +TEST(UtilDesktopTest, HasVector) { + EXPECT_FALSE(HasVector(Variant(10))); + EXPECT_FALSE(HasVector(Variant("A"))); + EXPECT_FALSE(HasVector(util::JsonToVariant("{\"A\":1}"))); + EXPECT_TRUE(HasVector(util::JsonToVariant("[1,2,3]"))); + EXPECT_TRUE(HasVector(util::JsonToVariant("{\"A\":[1,2,3]}"))); +} + +TEST(UtilDesktopTest, ParseInteger) { + int64_t number = 0; + EXPECT_TRUE(ParseInteger("0", &number)); + EXPECT_EQ(number, 0); + EXPECT_TRUE(ParseInteger("1", &number)); + EXPECT_EQ(number, 1); + EXPECT_TRUE(ParseInteger("-1", &number)); + EXPECT_EQ(number, -1); + EXPECT_TRUE(ParseInteger("+1", &number)); + EXPECT_EQ(number, 1); + EXPECT_TRUE(ParseInteger("1234", &number)); + EXPECT_EQ(number, 1234); + + EXPECT_TRUE(ParseInteger("00", &number)); + EXPECT_EQ(number, 0); + EXPECT_TRUE(ParseInteger("01", &number)); + EXPECT_EQ(number, 1); + EXPECT_TRUE(ParseInteger("-01", &number)); + EXPECT_EQ(number, -1); + + EXPECT_FALSE(ParseInteger("1234.1", &number)); + EXPECT_FALSE(ParseInteger("1 2 3", &number)); + EXPECT_FALSE(ParseInteger("ABC", &number)); + EXPECT_FALSE(ParseInteger("1B3", &number)); + EXPECT_FALSE(ParseInteger("123.A", &number)); +} + +TEST(UtilDesktopTest, PrunePrioritiesAndConvertVector) { + { + // 10 => 10 + Variant value = 10; + Variant expect = value; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {".value":10, ".priority":1} => 10 + Variant value = util::JsonToVariant("{\".value\":10,\".priority\":1}"); + Variant expect = 10; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":10, ".priority":1} => {"A":10} + Variant value = util::JsonToVariant("{\"A\":10,\".priority\":1}"); + Variant expect = util::JsonToVariant("{\"A\":10}"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":{"B":10,".priority":2},".priority":1} => {"A":{"B":10}} + Variant value = util::JsonToVariant( + "{\"A\":{\"B\":10,\".priority\":2},\".priority\":1}"); + Variant expect = util::JsonToVariant("{\"A\":{\"B\":10}}"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"1":1,"2":2} => [0,1,2] + Variant value = util::JsonToVariant("{\"0\":0,\"1\":1,\"2\":2}"); + Variant expect = util::JsonToVariant("[0,1,2]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"000000":0,"000001":1,"000002":2} => {"000000":0,"000001":1,"000002":2} + Variant value = + util::JsonToVariant("{\"000000\":0,\"000001\":1,\"000002\":2}"); + Variant expect = + util::JsonToVariant("{\"000000\":0,\"000001\":1,\"000002\":2}"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"2":2} => [0,null,2] + Variant value = util::JsonToVariant("{\"0\":0,\"2\":2}"); + Variant expect = util::JsonToVariant("[0,null,2]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // No change because more than half of the keys are missing (1, 2, 3) + // {"3":3} => {"3":3} + Variant value = util::JsonToVariant("{\"3\":3}"); + Variant expect = value; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // Change because less or equal to half of the keys are missing (0, 2) + // {"1":1,"3":3} => [null,1,null,3] + Variant value = util::JsonToVariant("{\"1\":1,\"3\":3}"); + Variant expect = util::JsonToVariant("[null,1,null,3]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"1":1,"A":"2"} => {"0":0,"1":1,"A":"2"} + Variant value = util::JsonToVariant("{\"0\":0,\"1\":1,\"A\":\"2\"}"); + Variant expect = value; + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":0,"1":1,".priority":1} => [0,1] + Variant value = util::JsonToVariant("{\"0\":0,\"1\":1,\".priority\":1}"); + Variant expect = util::JsonToVariant("[0,1]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":{"0":0,".priority":1},"1":1,".priority":1} => [[0],1] + Variant value = util::JsonToVariant( + "{\"0\":{\"0\":0,\".priority\":1},\"1\":1,\".priority\":1}"); + Variant expect = util::JsonToVariant("[[0],1]"); + PrunePrioritiesAndConvertVector(&value); + EXPECT_EQ(value, expect); + } +} + +TEST(UtilDesktopTest, PruneNullsRecursively) { + Variant value = std::map{ + std::make_pair("null", Variant::Null()), + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair( + "map", + std::map{ + std::make_pair("another_null", Variant::Null()), + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + std::make_pair("another_empty_map", Variant::EmptyMap()), + }), + std::make_pair("empty_map", Variant::EmptyMap()), + }; + + PruneNulls(&value, true); + + Variant expected = std::map{ + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair("map", + std::map{ + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + }), + }; + + EXPECT_EQ(value, expected); +} + +TEST(UtilDesktopTest, PruneNullsNonRecursively) { + Variant value = std::map{ + std::make_pair("null", Variant::Null()), + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair( + "map", + std::map{ + std::make_pair("another_null", Variant::Null()), + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + std::make_pair("another_empty_map", Variant::EmptyMap()), + }), + std::make_pair("empty_map", Variant::EmptyMap()), + }; + + PruneNulls(&value, false); + + Variant expected = std::map{ + std::make_pair("bool", false), + std::make_pair("int", 100), + std::make_pair("string", "I'm a string!"), + std::make_pair("float", 3.1415926), + std::make_pair( + "map", + std::map{ + std::make_pair("another_null", Variant::Null()), + std::make_pair("another_bool", true), + std::make_pair("another_int", 0), + std::make_pair("another_string", ""), + std::make_pair("another_float", 0.0), + std::make_pair("another_empty_map", Variant::EmptyMap()), + }), + }; + + EXPECT_EQ(value, expected); +} + +TEST(UtilDesktopTest, ConvertVectorToMap) { + { + // 10 => 10 + Variant value = 10; + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {".value":10, ".priority":1} => {".value":10, ".priority":1} + Variant value = util::JsonToVariant("{\".value\":10,\".priority\":1}"); + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":10, ".priority":1} => {"A":10, ".priority":1} + Variant value = util::JsonToVariant("{\"A\":10,\".priority\":1}"); + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {"A":{"B":10,".priority":2},".priority":1} => + // {"A":{"B":10,".priority":2},".priority":1} + Variant value = util::JsonToVariant( + "{\"A\":{\"B\":10,\".priority\":2},\".priority\":1}"); + Variant expect = value; + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // [0,1,2] => {"0":0,"1":1,"2":2} + Variant value = util::JsonToVariant("[0,1,2]"); + Variant expect = util::JsonToVariant("{\"0\":0,\"1\":1,\"2\":2}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // [[0,1],1,2] => {"0":{"0":0,"1":1},"1":1,"2":2} + Variant value = util::JsonToVariant("[[0,1],1,2]"); + Variant expect = + util::JsonToVariant("{\"0\":{\"0\":0,\"1\":1},\"1\":1,\"2\":2}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {"0":[0,1],".priority":1} => {"0":{"0":0,"1":1},".priority":1} + Variant value = util::JsonToVariant("{\"0\":[0,1],\".priority\":1}"); + Variant expect = + util::JsonToVariant("{\"0\":{\"0\":0,\"1\":1},\".priority\":1}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // {".value":[0,1,2],".priority":1} => {"0":0,"1":1,"2":2,".priority":1} + Variant value = util::JsonToVariant("{\".value\":[0,1,2],\".priority\":1}"); + Variant expect = + util::JsonToVariant("{\"0\":0,\"1\":1,\"2\":2,\".priority\":1}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } + + { + // Test for sanity + // {".value":[{".value":[0,1],".priority":3},1,2],".priority":1} => + // {"0":{"0":0,"1":1,".priority":3},"1":1,"2":2,".priority":1} + Variant value = util::JsonToVariant( + "{\".value\":[{\".value\":[0,1],\".priority\":3},1,2],\".priority\":" + "1}"); + Variant expect = util::JsonToVariant( + "{\"0\":{\"0\":0,\"1\":1,\".priority\":3},\"1\":1,\"2\":2,\"." + "priority\":1}"); + ConvertVectorToMap(&value); + EXPECT_EQ(value, expect); + } +} + +TEST(UtilDesktopTest, PrunePriorities_FundamentalType) { + // Ensure nothing happens. + Variant simple_value = 10; + Variant simple_value_copy = simple_value; + PrunePriorities(&simple_value); + EXPECT_EQ(simple_value, simple_value_copy); +} + +TEST(UtilDesktopTest, PrunePriorities_FundamentalTypeWithPriority) { + // Collapse the value/priority pair into just a value. + Variant high_priority_food = std::map{ + std::make_pair(".value", "pizza"), + std::make_pair(".priority", 10000), + }; + PrunePriorities(&high_priority_food); + EXPECT_THAT(high_priority_food.string_value(), StrEq("pizza")); +} + +TEST(UtilDesktopTest, PrunePriorities_MapWithPriority) { + // Remove priority field. + Variant worst_foods_with_priority = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + std::make_pair(".priority", -100000), + }; + Variant worst_foods = std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + }; + PrunePriorities(&worst_foods_with_priority); + EXPECT_EQ(worst_foods_with_priority, worst_foods); +} + +TEST(UtilDesktopTest, PrunePriorities_NestedMaps) { + // Correctly handle recursive maps. + Variant nested_map = std::map{ + std::make_pair("simple_value", 1), + std::make_pair("prioritized_value", + std::map{ + std::make_pair(".value", "pizza"), + std::make_pair(".priority", 10000), + }), + std::make_pair("prioritized_map", + std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + std::make_pair(".priority", -100000), + }), + }; + Variant nested_map_expectation = std::map{ + std::make_pair("simple_value", 1), + std::make_pair("prioritized_value", "pizza"), + std::make_pair("prioritized_map", + std::map{ + std::make_pair("bad", "peas"), + std::make_pair("badder", "asparagus"), + std::make_pair("baddest", "brussel sprouts"), + }), + }; + PrunePriorities(&nested_map); + EXPECT_EQ(nested_map, nested_map_expectation); +} + +TEST(UtilDesktopTest, GetVariantValueAndGetVariantPriority) { + // Test with Null priority + { + // Pairs of value and expected result + std::vector> test_cases = { + {"", ""}, // Variant::Null() + {"123", "123"}, + {"123.456", "123.456"}, + {"'string'", "'string'"}, + {"true", "true"}, + {"false", "false"}, + {"[1,2,3]", "[1,2,3]"}, + {"{'A':1,'B':'b','C':true}", "{'A':1,'B':'b','C':true}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant original_variant = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + + const Variant* value_ptr = GetVariantValue(&original_variant); + const Variant priority = GetVariantPriority(original_variant); + + EXPECT_NE(value_ptr, nullptr); + EXPECT_EQ(value_ptr, &original_variant); + EXPECT_EQ(*value_ptr, expected); + + EXPECT_EQ(priority, Variant::Null()); + } + } + + // Test with priority + { + // Pairs of value and expected result + std::vector> test_cases = { + {"{'.value':123,'.priority':100}", "123"}, + {"{'.value':123.456,'.priority':100}", "123.456"}, + {"{'.value':'string','.priority':100}", "'string'"}, + {"{'.value':true,'.priority':100}", "true"}, + {"{'.value':false,'.priority':100}", "false"}, + {"{'.value':[1,2,3],'.priority':100}", "[1,2,3]"}, + {"{'A':1,'B':'b','C':true,'.priority':100}", + "{'A':1,'B':'b','C':true,'.priority':100}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant original_variant = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + + const Variant* value_ptr = GetVariantValue(&original_variant); + const Variant& priority = GetVariantPriority(original_variant); + + EXPECT_TRUE(value_ptr != nullptr); + switch (value_ptr->type()) { + case Variant::kTypeNull: + case Variant::kTypeMap: + EXPECT_EQ(value_ptr, &original_variant); + break; + default: + EXPECT_EQ(value_ptr, &original_variant.map()[".value"]); + break; + } + EXPECT_EQ(*value_ptr, expected); + + EXPECT_EQ(priority, original_variant.map()[".priority"]); + EXPECT_EQ(priority, Variant::FromInt64(100)); + } + } +} + +TEST(UtilDesktopTest, CombineValueAndPriority) { + // Test with Null priority + { + Variant priority = Variant::Null(); + // Pairs of value and expected result. + std::vector> test_cases = { + {"", ""}, // Variant::Null() + {"123", "123"}, + {"123.456", "123.456"}, + {"'string'", "'string'"}, + {"true", "true"}, + {"false", "false"}, + {"[1,2,3]", "[1,2,3]"}, + {"{'A':1,'B':'b','C':true}", "{'A':1,'B':'b','C':true}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant value = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + EXPECT_THAT(CombineValueAndPriority(value, priority), Eq(expected)); + } + } + + // Test with priority + { + Variant priority = Variant::FromInt64(100); + // Pairs of value and expected result + std::vector> test_cases = { + {"", ""}, // Variant::Null() + {"123", "{'.value':123,'.priority':100}"}, + {"123.456", "{'.value':123.456,'.priority':100}"}, + {"'string'", "{'.value':'string','.priority':100}"}, + {"true", "{'.value':true,'.priority':100}"}, + {"false", "{'.value':false,'.priority':100}"}, + {"[1,2,3]", "{'.value':[1,2,3],'.priority':100}"}, + {"{'A':1,'B':'b','C':true}", + "{'A':1,'B':'b','C':true,'.priority':100}"}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", + "{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}"}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + std::replace(test.second.begin(), test.second.end(), '\'', '\"'); + + Variant value = util::JsonToVariant(test.first.c_str()); + Variant expected = util::JsonToVariant(test.second.c_str()); + EXPECT_THAT(CombineValueAndPriority(value, priority), Eq(expected)); + } + } +} + +TEST(UtilDesktopTest, VariantIsLeaf) { + // Pairs of value and expected result + std::vector> test_cases = { + {"", true}, + {"123", true}, + {"123.456", true}, + {"'string'", true}, + {"true", true}, + {"false", true}, + {"[1,2,3]", false}, + {"{'A':1,'B':'b','C':true}", false}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true}", false}, + {"{'.value':123,'.priority':100}", true}, + {"{'.value':123.456,'.priority':100}", true}, + {"{'.value':'string','.priority':100}", true}, + {"{'.value':true,'.priority':100}", true}, + {"{'.value':false,'.priority':100}", true}, + {"{'.value':[1,2,3],'.priority':100}", false}, + {"{'A':1,'B':'b','C':true,'.priority':100}", false}, + {"{'A':1,'B':{'.value':'b','.priority':100},'C':true,'.priority':100}", + false}, + }; + + for (auto& test : test_cases) { + // Replace all \' to \" to match Json format. Used \' for readability + std::replace(test.first.begin(), test.first.end(), '\'', '\"'); + + Variant original_variant = util::JsonToVariant(test.first.c_str()); + + EXPECT_THAT(VariantIsLeaf(original_variant), test.second); + } +} + +TEST(UtilDesktopTest, VariantIsEmpty) { + EXPECT_TRUE(VariantIsEmpty(Variant::Null())); + EXPECT_TRUE(VariantIsEmpty(Variant::EmptyMap())); + EXPECT_TRUE(VariantIsEmpty(Variant::EmptyVector())); + + EXPECT_FALSE(VariantIsEmpty(Variant::FromBool(false))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromBool(true))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromInt64(0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromInt64(9999))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromDouble(0.0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromDouble(1234.0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableString(""))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableString("lorem ipsum"))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromStaticString(""))); + EXPECT_FALSE(VariantIsEmpty( + Variant(std::map{std::make_pair("test", 10)}))); + EXPECT_FALSE(VariantIsEmpty(Variant(std::vector{1, 2, 3}))); + const char blob[] = {72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}; + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableBlob(nullptr, 0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromMutableBlob(blob, sizeof(blob)))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromStaticBlob(nullptr, 0))); + EXPECT_FALSE(VariantIsEmpty(Variant::FromStaticBlob(blob, sizeof(blob)))); +} + +TEST(UtilDesktopTest, VariantsAreEquivalent) { + // All of the regular comparisons should behave as expected. + EXPECT_TRUE(VariantsAreEquivalent(Variant::Null(), Variant::Null())); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromBool(false), + Variant::FromBool(false))); + EXPECT_TRUE( + VariantsAreEquivalent(Variant::FromBool(true), Variant::FromBool(true))); + EXPECT_TRUE( + VariantsAreEquivalent(Variant::FromInt64(100), Variant::FromInt64(100))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromInt64(100), + Variant::FromDouble(100.0f))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromMutableString("Hi"), + Variant::FromMutableString("Hi"))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromStaticString("Hi"), + Variant::FromStaticString("Hi"))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromStaticString("Hi"), + Variant::FromMutableString("Hi"))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromMutableString("Hi"), + Variant::FromStaticString("Hi"))); + + // Double to Int comparison should result in equal values despite different + // types. + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromDouble(100.0f), + Variant::FromInt64(100))); + EXPECT_TRUE(VariantsAreEquivalent(Variant::FromInt64(100), + Variant::FromDouble(100.0f))); + + EXPECT_FALSE(VariantsAreEquivalent(Variant::FromDouble(1000.0f), + Variant::FromInt64(100))); + EXPECT_FALSE( + VariantsAreEquivalent(Variant::FromDouble(3.14f), Variant::FromInt64(3))); + + // Maps should recursively check if children are also equivlanet. + Variant map_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant equal_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant equivalent_variant = std::map{ + std::make_pair("aaa", 100.0), + std::make_pair("bbb", 200.0), + std::make_pair("ccc", 300.0), + }; + Variant priority_variant = std::map{ + std::make_pair(".priority", 1), + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + EXPECT_TRUE(VariantsAreEquivalent(map_variant, equal_variant)); + EXPECT_TRUE(VariantsAreEquivalent(map_variant, equivalent_variant)); + EXPECT_FALSE(VariantsAreEquivalent(map_variant, priority_variant)); + + // Strings are not the same as ints to the database + Variant bad_string_variant = std::map{ + std::make_pair("aaa", "100"), + std::make_pair("bbb", "200"), + std::make_pair("ccc", "300"), + }; + // Variants that have too many elements should not compare equal, even if + // the elements they share are the same. + Variant too_long_variant = std::map{ + std::make_pair("aaa", "100"), + std::make_pair("bbb", "200"), + std::make_pair("ccc", "300"), + std::make_pair("ddd", "400"), + }; + EXPECT_FALSE(VariantsAreEquivalent(map_variant, bad_string_variant)); + EXPECT_FALSE(VariantsAreEquivalent(map_variant, too_long_variant)); + + // Same rules should apply to nested variants. + Variant nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }; + Variant equal_nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300), + std::make_pair("eee", 400), + }), + }; + Variant equivalent_nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300.0), + std::make_pair("eee", 400.0), + }), + }; + + EXPECT_TRUE(VariantsAreEquivalent(nested_variant, equal_nested_variant)); + EXPECT_TRUE(VariantsAreEquivalent(nested_variant, equivalent_nested_variant)); + + Variant bad_nested_variant = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", + std::map{ + std::make_pair("ddd", 300.0), + std::make_pair("eee", 400.0), + std::make_pair("fff", 500.0), + }), + }; + EXPECT_FALSE(VariantsAreEquivalent(nested_variant, bad_nested_variant)); +} + +TEST(UtilDesktopTest, GetBase64SHA1) { + std::vector> test_cases = { + {"", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="}, + {"i", "BC3EUS+j05HFFwzzqmHmpjj4Q0I="}, + {"ii", "ORg3PPVVnFS1LHBmQo9sQRjTHCM="}, + {"iii", "Ql/8FCLcTzJSi9n9WvNV/bXJYZI="}, + {"iiii", "MFMcKIXOYbOF3IHSo3X2vvgGB9U="}, + {"αβγωΑΒΓΩ", "WtUIYTivR0gge33nOEyQiBZGkmM="}, + }; + + std::string encoded; + for (auto& test : test_cases) { + EXPECT_THAT(GetBase64SHA1(test.first, &encoded), Eq(test.second)); + } +} + +TEST(UtilDesktopTest, ChildKeyCompareTo) { + // Expect left is equal to right + EXPECT_EQ(ChildKeyCompareTo(Variant("0"), Variant("0")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("1"), Variant("1")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("10"), Variant("10")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("A"), Variant("A")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("1A"), Variant("1A")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("[MIN_KEY]")), 0); + EXPECT_EQ(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("[MAX_KEY]")), 0); + + // Expect left is greater than right + EXPECT_GT(ChildKeyCompareTo(Variant("1"), Variant("0")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("0"), Variant("-1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1"), Variant("-1")), 0); + // "001" is equivalant to "1" in int value + EXPECT_GT(ChildKeyCompareTo(Variant("001"), Variant("-1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1"), Variant("-001")), 0); + // "001" is equivalant to "1" in int value but has longer length as a string + EXPECT_GT(ChildKeyCompareTo(Variant("001"), Variant("1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-001"), Variant("-1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("001"), Variant("-001")), 0); + // String is always greater than int + EXPECT_GT(ChildKeyCompareTo(Variant("A"), Variant("1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1A"), Variant("10")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-1A"), Variant("10")), 0); + // "-" is a string + EXPECT_GT(ChildKeyCompareTo(Variant("-"), Variant("10")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-"), Variant("-1")), 0); + // "1.1" is not an int, therefore treated as a string + EXPECT_GT(ChildKeyCompareTo(Variant("1.1"), Variant("10")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1.1"), Variant("0")), 0); + // Floating point is treated as string for comparison. + EXPECT_GT(ChildKeyCompareTo(Variant("11.1"), Variant("1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("1.1"), Variant("-1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-11.1"), Variant("-1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A"), Variant("1.1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A1"), Variant("A")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A2"), Variant("A1")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("AA"), Variant("A")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("AA"), Variant("A1")), 0); + // "[MIN_KEY]" is less than anything + EXPECT_GT(ChildKeyCompareTo(Variant("0"), Variant("[MIN_KEY]")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("-100000"), Variant("[MIN_KEY]")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("100000"), Variant("[MIN_KEY]")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("A"), Variant("[MIN_KEY]")), 0); + // "[MAX_KEY]" is greater than anything + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("0")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("1000000")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("-1000000")), 0); + EXPECT_GT(ChildKeyCompareTo(Variant("[MAX_KEY]"), Variant("A")), 0); + + // Expect left is less than right + EXPECT_LT(ChildKeyCompareTo(Variant("0"), Variant("1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("0")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("1")), 0); + // "001" is equivalant to "1" in int value + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("001")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-001"), Variant("1")), 0); + // "001" is equivalant to "1" in int value but has longer length as a string + EXPECT_LT(ChildKeyCompareTo(Variant("1"), Variant("001")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("-001")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-001"), Variant("001")), 0); + // String is always greater than int + EXPECT_LT(ChildKeyCompareTo(Variant("1"), Variant("A")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("1A")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("-1A")), 0); + // "-" is a string + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("-")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1"), Variant("-")), 0); + // "1.1" is not an int, therefore treated as a string + EXPECT_LT(ChildKeyCompareTo(Variant("10"), Variant("1.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("0"), Variant("1.1")), 0); + // Floating point is treated as string for comparison. + EXPECT_LT(ChildKeyCompareTo(Variant("1.1"), Variant("11.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1.1"), Variant("1.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-1.1"), Variant("-11.1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("1.1"), Variant("A")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A"), Variant("A1")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A1"), Variant("A2")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A"), Variant("AA")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A1"), Variant("AA")), 0); + // "[MIN_KEY]" is less than anything + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("0")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("-100000")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("100000")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("[MIN_KEY]"), Variant("A")), 0); + // "[MAX_KEY]" is greater than anything + EXPECT_LT(ChildKeyCompareTo(Variant("0"), Variant("[MAX_KEY]")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("100000"), Variant("[MAX_KEY]")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("-100000"), Variant("[MAX_KEY]")), 0); + EXPECT_LT(ChildKeyCompareTo(Variant("A"), Variant("[MAX_KEY]")), 0); +} + +TEST(UtilDesktopTest, GetHashRepresentation) { + std::vector> test_cases = { + // Null + {Variant::Null(), ""}, + // Int64 + {Variant(0), "number:0000000000000000"}, + {Variant(1), "number:3ff0000000000000"}, + {Variant::FromInt64(INT64_MIN), "number:c3e0000000000000"}, + // Double + {Variant(0.1), "number:3fb999999999999a"}, + {Variant(1.2345678901234567), "number:3ff3c0ca428c59fb"}, + {Variant(12345.678901234567), "number:40c81cd6e63c53d7"}, + {Variant(1234567890123456.5), "number:43118b54f22aeb02"}, + // Boolean + {Variant(true), "boolean:true"}, + {Variant(false), "boolean:false"}, + // String + {Variant("i"), "string:i"}, + {Variant("ii"), "string:ii"}, + {Variant("iii"), "string:iii"}, + {Variant("iiii"), "string:iiii"}, + // UTF-8 String + {Variant("αβγωΑΒΓΩ"), "string:αβγωΑΒΓΩ"}, + // Basic Map + {util::JsonToVariant("{\"B2\":2,\"B1\":1}"), + ":B1:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:B2:WtSt2Xo3L0JtPuArzQHofPrZOuU="}, + // Map with priority + {util::JsonToVariant( + "{\"B1\":{\".value\":1,\".priority\":2.0},\"B2\":{\".value\":2," + "\".priority\":1.0},\"B3\":3}"), + ":B3:3tYODYzGXwaGnXNech4jb4T9las=:B2:iiz9CIvYWkKdETTpjVFBJNx1SiI=" + ":B1:FvGzv2x5RbRTIc6uhMwY3pMW2oU="}, + // Array + {util::JsonToVariant("[1, 2, 3]"), + ":0:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las="}, + // Map in representation of an array + {util::JsonToVariant("{\"0\":1, \"1\":2, \"2\":3}"), + ":0:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las="}, + // Array more than 10 elements + {util::JsonToVariant("[7, 2, 3, 9, 5, 6, 1, 4, 8, 10, 11]"), + ":0:7wQgMram7RVqVIg/xRZWPfygGx0=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las=:3:M7Kyw8zsPkNHRw35uJ1vdPacr90=" + ":4:w28swksk9+tXf5jEdS9R5oSFAv8=:5:qb1N9GrUXfC3JyZPF8EXiNYcv4I=" + ":6:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:7:eVih19a6ZDz3NL32uVBtg9KSgQY=" + ":8:pITK737CVleu2Q4bHJTdQ4dJnCg=:9:+r5aI9HvKKagELki8SYKBk0q7D4=" + ":10:+aUUrIPmWZcSiV4ocCSLYRSFawE="}, + // Map in representation of an array more than 10 elements + {util::JsonToVariant( + "{\"0\":7, \"1\":2, \"2\":3, \"3\":9, \"4\":5, \"5\":6, \"6\":1, " + "\"7\":4, \"8\":8, \"9\":10, \"10\":11}"), + ":0:7wQgMram7RVqVIg/xRZWPfygGx0=:1:WtSt2Xo3L0JtPuArzQHofPrZOuU=" + ":2:3tYODYzGXwaGnXNech4jb4T9las=:3:M7Kyw8zsPkNHRw35uJ1vdPacr90=" + ":4:w28swksk9+tXf5jEdS9R5oSFAv8=:5:qb1N9GrUXfC3JyZPF8EXiNYcv4I=" + ":6:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:7:eVih19a6ZDz3NL32uVBtg9KSgQY=" + ":8:pITK737CVleu2Q4bHJTdQ4dJnCg=:9:+r5aI9HvKKagELki8SYKBk0q7D4=" + ":10:+aUUrIPmWZcSiV4ocCSLYRSFawE="}, + // Array with priority of different types + {util::JsonToVariant( + "[1,{\".value\":2,\".priority\":\"1\"},{\".value\":3,\".priority\":" + "1.1},{\".value\":4,\".priority\":1}]"), + ":0:YPVfR2bXt/lcDjiQZ8pOkAd3qkQ=:3:MTfbusV7VkrLc1KUkR7t8903AO0=" + ":2:McRf84Bik6f4pUV86mpvDCk7CIY=:1:xJPtZCG4C1Z2dsXLdmD4nuEeJWg="}, + // Map with mixed numeric and alphanumeric keys + {util::JsonToVariant("{\"1\":10, \"01\":7, \"001\":8, \"10\":20, " + "\"11\":29, \"12\":25, \"A\":15}"), + ":1:+r5aI9HvKKagELki8SYKBk0q7D4=:01:7wQgMram7RVqVIg/xRZWPfygGx0=" + ":001:pITK737CVleu2Q4bHJTdQ4dJnCg=:10:KAU+hDgZHcHeW8Ejndss7NJXOts=" + ":11:6+iMnJRA9k8I9jMianUFkJUZ2as=:12:EBgCJ72ufYyBZo/vQcusywSQr0k=" + ":A:o0Z01FiFkcaCNvXrl/rO9/d+zjk="}, + // LeafNode with priority + {util::JsonToVariant("{\".value\":2,\".priority\":1.0}"), + "priority:number:3ff0000000000000:number:4000000000000000"}, + // Map with priority + {util::JsonToVariant("{\".priority\":2.0,\"A\":2}"), + "priority:number:4000000000000000::A:WtSt2Xo3L0JtPuArzQHofPrZOuU="}, + // Nested priority + {util::JsonToVariant( + "{\".priority\":3.0,\"A\":{\".value\":2,\".priority\":1.0}}"), + "priority:number:4008000000000000::A:iiz9CIvYWkKdETTpjVFBJNx1SiI="}, + }; + + std::string hash_rep; + for (const auto& test : test_cases) { + EXPECT_THAT(GetHashRepresentation(test.first, &hash_rep), Eq(test.second)); + } +} + +TEST(UtilDesktopTest, GetHash) { + std::vector> test_cases = { + // Null + {Variant::Null(), ""}, + // Int64 + {Variant(0), "7ysMph9WPitGP7poMnMHMVPtUlI="}, + {Variant(1), "YPVfR2bXt/lcDjiQZ8pOkAd3qkQ="}, + {Variant::FromInt64(INT64_MIN), "t8Zsu6QlM7Q4staTHVsgiTYxyUs="}, + // Double + {Variant(0.1), "wtQjBi5TBE+ZcdekL6INiSeCSQI="}, + {Variant(1.2345678901234567), "xy9cBNnU0nPSZZ/ZhBUrD5JZHqI="}, + {Variant(12345.678901234567), "dY5swb32BtBwcxLG0QSzKrxF4Ek="}, + {Variant(1234567890123456.5), "TnvxroHDDUski72FbjG9s1opR2U="}, + // Boolean + {Variant(true), "E5z61QM0lN/U2WsOnusszCTkR8M="}, + {Variant(false), "aSSNoqcS4oQwJ2xxH20rvpp3zP0="}, + // String + {Variant("i"), "DeH+bYeyNKPWpoASovNpeBOhCLU="}, + {Variant("ii"), "bzF9bn9qYLhJmuc33tDqMMVtgkY="}, + {Variant("iii"), "vHKAStiyuxaQKEElU3MxAxJ+Pjk="}, + {Variant("iiii"), "vX9ogm9I6wB/x0t3LY9jfsgwRhs="}, + // UTF-8 String + {Variant("αβγωΑΒΓΩ"), "7VgSkcL0RRqd5MecDe/uvdDP/LM="}, + // Basic Map + {util::JsonToVariant("{\"B2\":2,\"B1\":1}"), + "saXm0YMzvotwh2WvsZFatveeAZk="}, + // Map with priority + {util::JsonToVariant( + "{\"B1\":{\".value\":1,\".priority\":2.0},\"B2\":{\".value\":2," + "\".priority\":1.0},\"B3\":3}"), + "9q4+gOobE1ozTZyb85m/iDxoYzY="}, + // Array + {util::JsonToVariant("[1, 2, 3]"), "h6XOC3OcidJlNC1Velmi3gphgQk="}, + // Map in representation of an array. + {util::JsonToVariant("{\"0\":1, \"1\":2, \"2\":3}"), + "h6XOC3OcidJlNC1Velmi3gphgQk="}, + // Array more than 10 elements + {util::JsonToVariant("[7, 2, 3, 9, 5, 6, 1, 4, 8, 10, 11]"), + "0iPsE+86XkEMyhTUqK19iX0O+/E="}, + // Map in representation of an array more than 10 elements + {util::JsonToVariant( + "{\"0\":7, \"1\":2, \"2\":3, \"3\":9, \"4\":5, \"5\":6, \"6\":1, " + "\"7\":4, \"8\":8, \"9\":10, \"10\":11}"), + "0iPsE+86XkEMyhTUqK19iX0O+/E="}, + // Array with priority of different types + {util::JsonToVariant( + "[1,{\".value\":2,\".priority\":\"1\"},{\".value\":3,\".priority\":" + "1.1},{\".value\":4,\".priority\":1}]"), + "PfCbiYP2e75wAxeBx078Rpag/as="}, + // Map with mixed numeric and alphanumeric keys + {util::JsonToVariant("{\"1\":10, \"01\":7, \"001\":8, \"10\":20, " + "\"11\":29, \"12\":25, \"A\":15}"), + "fYENO1aD55oc6I6f+FM+cv1Y1yc="}, + // LeafNode with priority + {util::JsonToVariant("{\".value\":2,\".priority\":1.0}"), + "iiz9CIvYWkKdETTpjVFBJNx1SiI="}, + // Map with priority + {util::JsonToVariant("{\".priority\":2.0,\"A\":2}"), + "1xHri2Z3/K1NzjMObwiYwEfgo18="}, + // Nested priority + {util::JsonToVariant( + "{\".priority\":3.0,\"A\":{\".value\":2,\".priority\":1.0}}"), + "YpFTODg262pl4OnB8L9w0QdeZpM="}, + }; + + std::string hash; + for (const auto& test : test_cases) { + EXPECT_THAT(GetHash(test.first, &hash), Eq(test.second)); + } +} + +TEST(UtilDesktopTest, QuerySpecLoadsAllData) { + QuerySpec spec_default; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_default)); + + QuerySpec spec_order_by_key; + spec_order_by_key.params.order_by = QueryParams::kOrderByKey; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_order_by_key)); + + QuerySpec spec_order_by_value; + spec_order_by_value.params.order_by = QueryParams::kOrderByValue; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_order_by_value)); + + QuerySpec spec_order_by_child; + spec_order_by_child.params.order_by = QueryParams::kOrderByChild; + spec_order_by_child.params.order_by_child = "baby_mario"; + EXPECT_TRUE(QuerySpecLoadsAllData(spec_order_by_child)); + + QuerySpec spec_start_at_value; + spec_start_at_value.params.start_at_value = 0; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_start_at_value)); + + QuerySpec spec_start_at_child_key; + spec_start_at_child_key.params.start_at_child_key = "a"; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_start_at_child_key)); + + QuerySpec spec_end_at_value; + spec_end_at_value.params.end_at_value = 9999; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_end_at_value)); + + QuerySpec spec_end_at_child_key; + spec_end_at_child_key.params.end_at_child_key = "z"; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_end_at_child_key)); + + QuerySpec spec_equal_to_value; + spec_equal_to_value.params.equal_to_value = 5000; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_equal_to_value)); + + QuerySpec spec_equal_to_child_key; + spec_equal_to_child_key.params.equal_to_child_key = "mn"; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_equal_to_child_key)); + + QuerySpec spec_limit_first; + spec_limit_first.params.limit_first = 10; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_limit_first)); + + QuerySpec spec_limit_last; + spec_limit_last.params.limit_last = 20; + EXPECT_FALSE(QuerySpecLoadsAllData(spec_limit_last)); +} + +TEST(UtilDesktopTest, QuerySpecIsDefault) { + QuerySpec spec_default; + EXPECT_TRUE(QuerySpecIsDefault(spec_default)); + + QuerySpec spec_order_by_key; + spec_order_by_key.params.order_by = QueryParams::kOrderByKey; + EXPECT_FALSE(QuerySpecIsDefault(spec_order_by_key)); + + QuerySpec spec_order_by_value; + spec_order_by_value.params.order_by = QueryParams::kOrderByValue; + EXPECT_FALSE(QuerySpecIsDefault(spec_order_by_value)); + + QuerySpec spec_order_by_child; + spec_order_by_child.params.order_by = QueryParams::kOrderByChild; + spec_order_by_child.params.order_by_child = "baby_mario"; + EXPECT_FALSE(QuerySpecIsDefault(spec_order_by_child)); + + QuerySpec spec_start_at_value; + spec_start_at_value.params.start_at_value = 0; + EXPECT_FALSE(QuerySpecIsDefault(spec_start_at_value)); + + QuerySpec spec_start_at_child_key; + spec_start_at_child_key.params.start_at_child_key = "a"; + EXPECT_FALSE(QuerySpecIsDefault(spec_start_at_child_key)); + + QuerySpec spec_end_at_value; + spec_end_at_value.params.end_at_value = 9999; + EXPECT_FALSE(QuerySpecIsDefault(spec_end_at_value)); + + QuerySpec spec_end_at_child_key; + spec_end_at_child_key.params.end_at_child_key = "z"; + EXPECT_FALSE(QuerySpecIsDefault(spec_end_at_child_key)); + + QuerySpec spec_equal_to_value; + spec_equal_to_value.params.equal_to_value = 5000; + EXPECT_FALSE(QuerySpecIsDefault(spec_equal_to_value)); + + QuerySpec spec_equal_to_child_key; + spec_equal_to_child_key.params.equal_to_child_key = "mn"; + EXPECT_FALSE(QuerySpecIsDefault(spec_equal_to_child_key)); + + QuerySpec spec_limit_first; + spec_limit_first.params.limit_first = 10; + EXPECT_FALSE(QuerySpecIsDefault(spec_limit_first)); + + QuerySpec spec_limit_last; + spec_limit_last.params.limit_last = 20; + EXPECT_FALSE(QuerySpecIsDefault(spec_limit_last)); +} + +TEST(UtilDesktopTest, MakeDefaultQuerySpec) { + QuerySpec spec_default; + spec_default.path = Path("this/value/should/not/change"); + QuerySpec default_result = MakeDefaultQuerySpec(spec_default); + EXPECT_TRUE(QuerySpecIsDefault(default_result)); + EXPECT_EQ(default_result, spec_default); + + QuerySpec spec_featureful; + spec_featureful.path = Path("this/value/should/not/change"); + spec_featureful.params.order_by = QueryParams::kOrderByChild; + spec_featureful.params.order_by_child = "baby_mario"; + spec_featureful.params.start_at_value = 0; + spec_featureful.params.start_at_child_key = "a"; + spec_featureful.params.end_at_value = 9999; + spec_featureful.params.end_at_child_key = "z"; + spec_featureful.params.limit_first = 10; + spec_featureful.params.limit_last = 20; + QuerySpec featureful_result = MakeDefaultQuerySpec(spec_featureful); + EXPECT_TRUE(QuerySpecIsDefault(featureful_result)); + EXPECT_EQ(featureful_result, spec_default); +} + +TEST(UtilDesktopTest, WireProtocolPathToString) { + EXPECT_EQ(WireProtocolPathToString(Path()), "/"); + EXPECT_EQ(WireProtocolPathToString(Path("")), "/"); + EXPECT_EQ(WireProtocolPathToString(Path("/")), "/"); + EXPECT_EQ(WireProtocolPathToString(Path("///")), "/"); + + EXPECT_EQ(WireProtocolPathToString(Path("A")), "A"); + EXPECT_EQ(WireProtocolPathToString(Path("/A")), "A"); + EXPECT_EQ(WireProtocolPathToString(Path("A/")), "A"); + EXPECT_EQ(WireProtocolPathToString(Path("/A/")), "A"); + + EXPECT_EQ(WireProtocolPathToString(Path("A/B")), "A/B"); + EXPECT_EQ(WireProtocolPathToString(Path("/A/B")), "A/B"); + EXPECT_EQ(WireProtocolPathToString(Path("A/B/")), "A/B"); + EXPECT_EQ(WireProtocolPathToString(Path("/A/B/")), "A/B"); +} + +TEST(UtilDesktopTest, GetWireProtocolParams) { + { + QueryParams params_default; + EXPECT_EQ(GetWireProtocolParams(params_default), Variant::EmptyMap()); + } + + { + QueryParams params; + params.start_at_value = "0"; + + Variant expected(std::map{ + {"sp", "0"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.start_at_value = 0; + params.start_at_child_key = "0010"; + + Variant expected(std::map{ + {"sp", 0}, + {"sn", "0010"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.end_at_value = "0"; + + Variant expected(std::map{ + {"ep", "0"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.end_at_value = 0; + params.end_at_child_key = "0010"; + + Variant expected(std::map{ + {"ep", 0}, + {"en", "0010"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.equal_to_value = 3.14; + + Variant expected(std::map{ + {"sp", 3.14}, + {"ep", 3.14}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.equal_to_value = 3.14; + params.equal_to_child_key = "A"; + + Variant expected(std::map{ + {"sp", 3.14}, + {"sn", "A"}, + {"ep", 3.14}, + {"en", "A"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.limit_first = 10; + + Variant expected(std::map{ + {"l", 10}, + {"vf", "l"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.limit_last = 20; + + Variant expected(std::map{ + {"l", 20}, + {"vf", "r"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "A"; + + Variant expected(std::map{ + {"i", ".key"}, + {"sp", "A"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.end_at_value = "Z"; + + Variant expected(std::map{ + {"i", ".value"}, + {"ep", "Z"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = ""; + params.limit_first = 10; + + Variant expected(std::map{ + {"i", "/"}, + {"l", 10}, + {"vf", "l"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } + + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "/A/B/C/"; + params.limit_last = 20; + + Variant expected(std::map{ + {"i", "A/B/C"}, + {"l", 20}, + {"vf", "r"}, + }); + + EXPECT_EQ(GetWireProtocolParams(params), expected); + } +} + +TEST(UtilDesktopTest, TestGetAppDataPath) { + // Make sure we get a path string. + EXPECT_NE(GetAppDataPath("testapp0"), ""); + + // Make sure we get 2 different paths for 2 different apps. + EXPECT_NE(GetAppDataPath("testapp1"), GetAppDataPath("testapp2")); + + // Make sure we get the same path if we are calling twice with the same app. + EXPECT_EQ(GetAppDataPath("testapp3"), GetAppDataPath("testapp3")); + + // Make sure the path string refers to a directory that is available. + std::string path = GetAppDataPath("testapp4", true); + struct stat s; + ASSERT_EQ(stat(path.c_str(), &s), 0) + << "stat failed on '" << path << "': " << strerror(errno); + EXPECT_TRUE(s.st_mode & S_IFDIR) << path << " is not a directory!"; + + // Write random data to a randomly generated filename. + std::string test_data = + std::string("Hello, world! ") + std::to_string(rand()); // NOLINT + std::string test_path = path + kPathSep + "test_file_" + + std::to_string(rand()) + ".txt"; // NOLINT + + // Ensure that we can save files in this directory. + FILE* out = fopen(test_path.c_str(), "w"); + EXPECT_NE(out, nullptr) << "Couldn't open test file for writing: " + << strerror(errno); + EXPECT_GE(fputs(test_data.c_str(), out), 0) << strerror(errno); + EXPECT_EQ(fclose(out), 0) << strerror(errno); + + FILE* in = fopen(test_path.c_str(), "r"); + EXPECT_NE(in, nullptr) << "Couldn't open test file for reading: " + << strerror(errno); + char buf[256]; + EXPECT_NE(fgets(buf, sizeof(buf), in), nullptr) << strerror(errno); + EXPECT_STREQ(buf, test_data.c_str()); + EXPECT_EQ(fclose(in), 0) << strerror(errno); + + // Delete the file. + EXPECT_EQ(unlink(test_path.c_str()), 0) << strerror(errno); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/change_test.cc b/database/tests/desktop/view/change_test.cc new file mode 100644 index 0000000000..e76d466b08 --- /dev/null +++ b/database/tests/desktop/view/change_test.cc @@ -0,0 +1,341 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/change.h" + +#include "app/src/include/firebase/variant.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(Change, DefaultConstructor) { + Change change; + EXPECT_EQ(change.indexed_variant.variant(), Variant::Null()); + EXPECT_EQ(change.child_key, ""); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, CopyConstructor) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("string variant")); + change.child_key = "Hello"; + change.prev_name = "World"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change copy_constructed(change); + EXPECT_EQ(copy_constructed.event_type, kEventTypeValue); + EXPECT_EQ(copy_constructed.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(copy_constructed.child_key, "Hello"); + EXPECT_EQ(copy_constructed.prev_name, "World"); + EXPECT_EQ(copy_constructed.old_indexed_variant.variant(), + Variant(1234567890)); + + Change copy_assigned; + copy_assigned = change; + EXPECT_EQ(copy_assigned.event_type, kEventTypeValue); + EXPECT_EQ(copy_assigned.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(copy_assigned.child_key, "Hello"); + EXPECT_EQ(copy_assigned.prev_name, "World"); + EXPECT_EQ(copy_assigned.old_indexed_variant.variant(), Variant(1234567890)); +} + +TEST(Change, MoveConstructor) { + { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("string variant")); + change.child_key = "Hello"; + change.prev_name = "World"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change move_constructed(std::move(change)); + EXPECT_EQ(move_constructed.event_type, kEventTypeValue); + EXPECT_EQ(move_constructed.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(move_constructed.child_key, "Hello"); + EXPECT_EQ(move_constructed.prev_name, "World"); + EXPECT_EQ(move_constructed.old_indexed_variant.variant(), + Variant(1234567890)); + } + + { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("string variant")); + change.child_key = "Hello"; + change.prev_name = "World"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change move_assigned; + move_assigned = change; + EXPECT_EQ(move_assigned.event_type, kEventTypeValue); + EXPECT_EQ(move_assigned.indexed_variant.variant(), + IndexedVariant(Variant("string variant")).variant()); + EXPECT_EQ(move_assigned.child_key, "Hello"); + EXPECT_EQ(move_assigned.prev_name, "World"); + EXPECT_EQ(move_assigned.old_indexed_variant.variant(), Variant(1234567890)); + } +} + +TEST(Change, Constructors) { + Change type_variant(kEventTypeValue, + IndexedVariant(Variant("abcdefghijklmnopqrstuvwxyz"))); + + EXPECT_EQ(type_variant.event_type, kEventTypeValue); + EXPECT_EQ(type_variant.indexed_variant.variant(), + Variant("abcdefghijklmnopqrstuvwxyz")); + EXPECT_EQ(type_variant.child_key, ""); + EXPECT_EQ(type_variant.prev_name, ""); + EXPECT_EQ(type_variant.old_indexed_variant.variant(), Variant::Null()); + + Change type_variant_string( + kEventTypeChildChanged, + IndexedVariant(Variant("zyxwvutsrqponmlkjihgfedcba")), "child_key"); + EXPECT_EQ(type_variant_string.event_type, kEventTypeChildChanged); + EXPECT_EQ(type_variant_string.indexed_variant.variant(), + Variant("zyxwvutsrqponmlkjihgfedcba")); + EXPECT_EQ(type_variant_string.child_key, "child_key"); + EXPECT_EQ(type_variant_string.prev_name, ""); + EXPECT_EQ(type_variant_string.old_indexed_variant.variant(), Variant::Null()); + + Change all_values_set(kEventTypeChildRemoved, + IndexedVariant(Variant("ABCDEFGHIJKLMNOPQRSTUVWXYZ")), + "another_child_key", "previous_child", + IndexedVariant(Variant("ZYXWVUSTRQPONMLKJIHGFEDCBA"))); + EXPECT_EQ(all_values_set.event_type, kEventTypeChildRemoved); + EXPECT_EQ(all_values_set.indexed_variant.variant(), + Variant("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + EXPECT_EQ(all_values_set.child_key, "another_child_key"); + EXPECT_EQ(all_values_set.prev_name, "previous_child"); + EXPECT_EQ(all_values_set.old_indexed_variant.variant(), + Variant("ZYXWVUSTRQPONMLKJIHGFEDCBA")); +} + +TEST(Change, ValueChange) { + Change change = ValueChange(IndexedVariant(Variant("ValueChanged!"))); + + EXPECT_EQ(change.event_type, kEventTypeValue); + EXPECT_EQ(change.indexed_variant.variant(), + IndexedVariant(Variant("ValueChanged!")).variant()); + EXPECT_EQ(change.child_key, ""); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChildAddedChange) { + Change change = + ChildAddedChange("child_key", IndexedVariant(Variant("ValueChanged!"))); + + EXPECT_EQ(change.event_type, kEventTypeChildAdded); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ValueChanged!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); + + Change another_change = + ChildAddedChange("another_child_key", Variant("!ChangedValue")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildAdded); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!ChangedValue")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChildRemovedChange) { + Change change = + ChildRemovedChange("child_key", IndexedVariant(Variant("ChildRemoved!"))); + + EXPECT_EQ(change.event_type, kEventTypeChildRemoved); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ChildRemoved!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); + + Change another_change = + ChildRemovedChange("another_child_key", Variant("!RemovedChild")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildRemoved); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!RemovedChild")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChildChangedChange) { + Change change = + ChildChangedChange("child_key", IndexedVariant(Variant("ChildChanged!")), + IndexedVariant(Variant("old value"))); + + EXPECT_EQ(change.event_type, kEventTypeChildChanged); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ChildChanged!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant("old value")); + + Change another_change = ChildChangedChange( + "another_child_key", Variant("!ChangedChild"), Variant("previous value")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildChanged); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!ChangedChild")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), + Variant("previous value")); +} + +TEST(Change, ChildMovedChange) { + Change change = + ChildMovedChange("child_key", IndexedVariant(Variant("ChildChanged!"))); + + EXPECT_EQ(change.event_type, kEventTypeChildMoved); + EXPECT_EQ(change.indexed_variant.variant(), Variant("ChildChanged!")); + EXPECT_EQ(change.child_key, "child_key"); + EXPECT_EQ(change.prev_name, ""); + EXPECT_EQ(change.old_indexed_variant.variant(), Variant::Null()); + + Change another_change = + ChildMovedChange("another_child_key", Variant("!ChangedChild")); + + EXPECT_EQ(another_change.event_type, kEventTypeChildMoved); + EXPECT_EQ(another_change.indexed_variant.variant(), Variant("!ChangedChild")); + EXPECT_EQ(another_change.child_key, "another_child_key"); + EXPECT_EQ(another_change.prev_name, ""); + EXPECT_EQ(another_change.old_indexed_variant.variant(), Variant::Null()); +} + +TEST(Change, ChangeWithPrevName) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("value")); + change.child_key = "child_key"; + change.prev_name = ""; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change result = ChangeWithPrevName(change, "prev_name"); + + EXPECT_EQ(result.event_type, kEventTypeValue); + EXPECT_EQ(result.indexed_variant.variant(), + IndexedVariant("value").variant()); + EXPECT_EQ(result.child_key, "child_key"); + EXPECT_EQ(result.prev_name, "prev_name"); + EXPECT_EQ(result.old_indexed_variant.variant(), Variant(1234567890)); +} + +TEST(Change, EqualityOperatorSame) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("value")); + change.child_key = "child_key"; + change.prev_name = "prev_name"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change identical_change; + identical_change.event_type = kEventTypeValue; + identical_change.indexed_variant = IndexedVariant(Variant("value")); + identical_change.child_key = "child_key"; + identical_change.prev_name = "prev_name"; + identical_change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + // Verify the == and != operators return the expected result. + // Check equality with self. + EXPECT_TRUE(change == change); + EXPECT_FALSE(change != change); + + // Check equality with an identical change. + EXPECT_TRUE(change == identical_change); + EXPECT_FALSE(change != identical_change); +} + +TEST(Change, EqualityOperatorDifferent) { + Change change; + change.event_type = kEventTypeValue; + change.indexed_variant = IndexedVariant(Variant("value")); + change.child_key = "child_key"; + change.prev_name = "prev_name"; + change.old_indexed_variant = IndexedVariant(Variant(1234567890)); + + Change change_different_type; + change_different_type.event_type = kEventTypeChildAdded; + change_different_type.indexed_variant = IndexedVariant(Variant("value")); + change_different_type.child_key = "child_key"; + change_different_type.prev_name = "prev_name"; + change_different_type.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_indexed_variant; + change_different_indexed_variant.event_type = kEventTypeValue; + change_different_indexed_variant.indexed_variant = + IndexedVariant(Variant("aeluv")); + change_different_indexed_variant.child_key = "child_key"; + change_different_indexed_variant.prev_name = "prev_name"; + change_different_indexed_variant.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_child_key; + change_different_child_key.event_type = kEventTypeValue; + change_different_child_key.indexed_variant = IndexedVariant(Variant("value")); + change_different_child_key.child_key = "cousin_key"; + change_different_child_key.prev_name = "prev_name"; + change_different_child_key.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_prev_name; + change_different_prev_name.event_type = kEventTypeValue; + change_different_prev_name.indexed_variant = IndexedVariant(Variant("value")); + change_different_prev_name.child_key = "child_key"; + change_different_prev_name.prev_name = "next_name"; + change_different_prev_name.old_indexed_variant = + IndexedVariant(Variant(1234567890)); + + Change change_different_old_indexed_variant; + change_different_old_indexed_variant.event_type = kEventTypeValue; + change_different_old_indexed_variant.indexed_variant = + IndexedVariant(Variant("value")); + change_different_old_indexed_variant.child_key = "child_key"; + change_different_old_indexed_variant.prev_name = "prev_name"; + change_different_old_indexed_variant.old_indexed_variant = + IndexedVariant(Variant(int64_t(9876543210))); + + // Verify the == and != operators return the expected result. + EXPECT_FALSE(change == change_different_type); + EXPECT_TRUE(change != change_different_type); + + EXPECT_FALSE(change == change_different_indexed_variant); + EXPECT_TRUE(change != change_different_indexed_variant); + + EXPECT_FALSE(change == change_different_child_key); + EXPECT_TRUE(change != change_different_child_key); + + EXPECT_FALSE(change == change_different_prev_name); + EXPECT_TRUE(change != change_different_prev_name); + + EXPECT_FALSE(change == change_different_old_indexed_variant); + EXPECT_TRUE(change != change_different_old_indexed_variant); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/child_change_accumulator_test.cc b/database/tests/desktop/view/child_change_accumulator_test.cc new file mode 100644 index 0000000000..78333b252f --- /dev/null +++ b/database/tests/desktop/view/child_change_accumulator_test.cc @@ -0,0 +1,182 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/child_change_accumulator.h" +#include "database/src/desktop/view/change.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +// Test to add new change data to the accumulator. +TEST(ChildChangeAccumulator, TrackChildChangeNew) { + // Add single ChildAdded change to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change = ChildAddedChange("ChildAdd", 1); + TrackChildChange(change, &accumulator); + + auto it = accumulator.find("ChildAdd"); + ASSERT_NE(it, accumulator.end()); + + EXPECT_EQ(it->second, change); + } + // Add single ChildChanged change to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change = ChildChangedChange("ChildChange", "new", "old"); + TrackChildChange(change, &accumulator); + + auto it = accumulator.find("ChildChange"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, change); + } + // Add single ChildRemoved change to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change = ChildRemovedChange("ChildRemove", true); + TrackChildChange(change, &accumulator); + + auto it = accumulator.find("ChildRemove"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, change); + } + // Add all ChildAdded, ChildChanged, ChildRemoved change with different child + // key to the accumulator. + { + ChildChangeAccumulator accumulator; + Change change_add = ChildAddedChange("ChildAdd", 1); + TrackChildChange(change_add, &accumulator); + + Change change_change = ChildChangedChange("ChildChange", "new", "old"); + TrackChildChange(change_change, &accumulator); + + Change change_remove = ChildRemovedChange("ChildRemove", true); + TrackChildChange(change_remove, &accumulator); + + // Verify child "ChildAdd" + auto it_add = accumulator.find("ChildAdd"); + ASSERT_NE(it_add, accumulator.end()); + EXPECT_EQ(it_add->second, change_add); + + // Verify child "ChildChange" + auto it_change = accumulator.find("ChildChange"); + ASSERT_NE(it_change, accumulator.end()); + EXPECT_EQ(it_change->second, change_change); + + // Verify child "ChildRemove" + auto it_remove = accumulator.find("ChildRemove"); + ASSERT_NE(it_remove, accumulator.end()); + EXPECT_EQ(it_remove->second, change_remove); + } +} + +// Test to resolve ChildRemoved change and ChildAdded change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeRemovedThenAdded) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildRemovedChange("ChildRemoveThenAdd", "old"), + &accumulator); + TrackChildChange(ChildAddedChange("ChildRemoveThenAdd", "new"), &accumulator); + + // Expected result should be a ChildChanged change from "old" to "new" + Change expected = ChildChangedChange("ChildRemoveThenAdd", "new", "old"); + + auto it = accumulator.find("ChildRemoveThenAdd"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +// Test to resolve ChildAdded change and ChildRemoved change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeAddedThenRemoved) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildAddedChange("ChildAddThenRemove", 1), &accumulator); + // Note: the removed value "true" does not need to match the value "1" added + // previously. + TrackChildChange(ChildRemovedChange("ChildAddThenRemove", true), + &accumulator); + + // Expect the child data to be removed + auto it = accumulator.find("ChildAddAndRemove"); + ASSERT_EQ(it, accumulator.end()); +} + +// Test to resolve ChildChanged change and ChildRemoved change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeChangedThenRemoved) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildChangedChange("ChildChangeThenRemove", "old", "order"), + &accumulator); + // Note: the removed value "new" does not need to match the value "old" + // changed previously. + TrackChildChange(ChildRemovedChange("ChildChangeThenRemove", "new"), + &accumulator); + + // Expected result should be a ChildRemoved change from "old" value + Change expected = ChildRemovedChange("ChildChangeThenRemove", "old"); + + auto it = accumulator.find("ChildChangeThenRemove"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +// Test to resolve ChildAdded change and ChildChanged change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeAddedThenChanged) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildAddedChange("ChildAddThenChange", "old"), &accumulator); + // Note: the old value "something else" does not need to match the value "old" + // added previously. + TrackChildChange( + ChildChangedChange("ChildAddThenChange", "new", "something else"), + &accumulator); + + // Expected result should be a ChildAdded change with "new" value + Change expected = ChildAddedChange("ChildAddThenChange", "new"); + + auto it = accumulator.find("ChildAddThenChange"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +// Test to resolve ChildChanged change and ChildChanged change with the same key +// in sequence. +TEST(ChildChangeAccumulator, TrackChildChangeChangedThenChanged) { + ChildChangeAccumulator accumulator; + TrackChildChange(ChildChangedChange("ChildChangeThenChange", "old", "older"), + &accumulator); + // Note: the old value "something else" does not need to match the value "old" + // changed previously. + TrackChildChange( + ChildChangedChange("ChildChangeThenChange", "new", "something else"), + &accumulator); + + // Expected result should be a ChildChanged change from "older" to "new". + Change expected = ChildChangedChange("ChildChangeThenChange", "new", "older"); + + auto it = accumulator.find("ChildChangeThenChange"); + ASSERT_NE(it, accumulator.end()); + EXPECT_EQ(it->second, expected); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/event_generator_test.cc b/database/tests/desktop/view/event_generator_test.cc new file mode 100644 index 0000000000..5ebc3e40a2 --- /dev/null +++ b/database/tests/desktop/view/event_generator_test.cc @@ -0,0 +1,346 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/event_generator.h" + +#include + +#include "app/src/include/firebase/variant.h" +#include "app/src/path.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/child_event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/util_desktop.h" + +using testing::Eq; +using testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { + +class EventGeneratorTest : public testing::Test { + public: + void SetUp() override { + query_spec_.path = Path("prefix/path"); + data_cache_ = Variant(std::map{ + std::make_pair("aaa", CombineValueAndPriority(100, 1)), + std::make_pair("bbb", CombineValueAndPriority(200, 2)), + std::make_pair("ccc", CombineValueAndPriority(300, 3)), + std::make_pair("ddd", CombineValueAndPriority(400, 4)), + }); + event_cache_ = IndexedVariant(data_cache_, query_spec_.params); + value_registration_ = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + child_registration_ = + new ChildEventRegistration(nullptr, nullptr, QuerySpec()); + event_registrations_ = std::vector>{ + UniquePtr(value_registration_), + UniquePtr(child_registration_), + }; + } + + protected: + QuerySpec query_spec_; + Variant data_cache_; + IndexedVariant event_cache_; + ValueEventRegistration* value_registration_; + ChildEventRegistration* child_registration_; + std::vector> event_registrations_; +}; + +class EventGeneratorDeathTest : public EventGeneratorTest {}; + +TEST_F(EventGeneratorTest, GenerateEventsForChangesAllAdded) { + std::vector changes{ + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("ccc", CombineValueAndPriority(300, 3)), + ChildAddedChange("ddd", CombineValueAndPriority(400, 4)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + std::vector expected{ + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesAllAddedReverseOrder) { + std::vector changes{ + ChildAddedChange("ddd", CombineValueAndPriority(400, 4)), + ChildAddedChange("ccc", CombineValueAndPriority(300, 3)), + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the query_spec's comparison + // rules. In this case, based on priority. + std::vector expected{ + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesDifferentTypes) { + std::vector changes{ + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 3)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the EventType. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesSomeDifferentTypes) { + std::vector changes{ + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildChangedChange("ddd", CombineValueAndPriority(400, 4), + CombineValueAndPriority(400, 4)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 3)), + ChildRemovedChange("fff", CombineValueAndPriority(600, 6)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the EventType and the + // query_spec's comparison rules. In this case, based on priority. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(600, 6), + QuerySpec(Path("prefix/path/fff"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesWithDifferentPriorities) { + std::vector changes{ + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + // The priorities of ccc and ddd are reversed in the old snapshot. + ChildChangedChange("ddd", CombineValueAndPriority(400, 4), + CombineValueAndPriority(400, 3)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 4)), + ChildRemovedChange("fff", CombineValueAndPriority(600, 6)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + std::vector result = GenerateEventsForChanges( + query_spec_, changes, event_cache_, event_registrations_); + + // The events are sorted into order based on the EventType and the + // query_spec's comparison rules. In this case, based on priority. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(600, 6), + QuerySpec(Path("prefix/path/fff"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + // Moving the priority generated both move and change events. + Event(kEventTypeChildMoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildMoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorTest, GenerateEventsForChangesWithDifferentQuerySpec) { + std::vector changes{ + ChildAddedChange("bbb", CombineValueAndPriority(200, 2)), + ChildAddedChange("aaa", CombineValueAndPriority(100, 1)), + ChildChangedChange("ddd", CombineValueAndPriority(400, 4), + CombineValueAndPriority(400, 3)), + ChildChangedChange("ccc", CombineValueAndPriority(300, 3), + CombineValueAndPriority(300, 4)), + ChildRemovedChange("fff", CombineValueAndPriority(600, 6)), + ChildRemovedChange("eee", CombineValueAndPriority(500, 5)), + }; + + // Changing the priority doesn't matter when the QuerySpec does not consider + // priority (e.g., when it orders the elements by value). + QuerySpec value_query_spec = query_spec_; + value_query_spec.params.order_by = QueryParams::kOrderByValue; + + std::vector result = GenerateEventsForChanges( + value_query_spec, changes, event_cache_, event_registrations_); + + // No move events this time around even though the priorities changed because + // the QuerySpec isn't ordered by priority, it's ordered by value. + std::vector expected{ + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(500, 5), + QuerySpec(Path("prefix/path/eee"))), + ""), + Event(kEventTypeChildRemoved, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(600, 6), + QuerySpec(Path("prefix/path/fff"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(100, 1), + QuerySpec(Path("prefix/path/aaa"))), + ""), + Event(kEventTypeChildAdded, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(200, 2), + QuerySpec(Path("prefix/path/bbb"))), + "aaa"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(300, 3), + QuerySpec(Path("prefix/path/ccc"))), + "bbb"), + Event(kEventTypeChildChanged, child_registration_, + DataSnapshotInternal(nullptr, CombineValueAndPriority(400, 4), + QuerySpec(Path("prefix/path/ddd"))), + "ccc"), + }; + + EXPECT_THAT(result, Pointwise(Eq(), expected)); +} + +TEST_F(EventGeneratorDeathTest, MissingChildName) { + std::vector changes{ + ChildAddedChange("", CombineValueAndPriority(100, 1)), + }; + // All child changes are expected to have a key. Missing a key means we have a + // malformed Change object. + EXPECT_DEATH(GenerateEventsForChanges(QuerySpec(), changes, event_cache_, + event_registrations_), + DEATHTEST_SIGABRT); +} + +TEST_F(EventGeneratorDeathTest, MultipleValueChanges) { + std::vector changes{ + ValueChange(IndexedVariant(Variant("aaa"))), + ValueChange(IndexedVariant(Variant("bbb"))), + }; + // Value changes only occur one at a time, so if we have two something has + // gone wrong at the call site. + EXPECT_DEATH(GenerateEventsForChanges(QuerySpec(), changes, event_cache_, + event_registrations_), + DEATHTEST_SIGABRT); +} + +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/indexed_filter_test.cc b/database/tests/desktop/view/indexed_filter_test.cc new file mode 100644 index 0000000000..96a4d9ce7a --- /dev/null +++ b/database/tests/desktop/view/indexed_filter_test.cc @@ -0,0 +1,391 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/indexed_filter.h" +#include "app/src/variant_util.h" +#include "database/src/common/query_spec.h" +#include "database/src/desktop/core/indexed_variant.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(IndexedFilter, UpdateChild_SameValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("aaa"); + Variant new_child(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }); + Path affected_path("bbb/ccc"); + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(old_child); + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + // Expect no changes + EXPECT_EQ(change_accumulator, ChildChangeAccumulator()); +} + +TEST(IndexedFilter, UpdateChild_ChangedValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("aaa"); + Variant new_child(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 200), + }), + }); + Path affected_path("bbb/ccc"); + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(std::map{ + std::make_pair("aaa", new_child), + }); + ChildChangeAccumulator expected_changes{ + std::make_pair( + "aaa", + ChildChangedChange("aaa", new_child, + Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }))), + }; + + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + EXPECT_EQ(change_accumulator, expected_changes); +} + +TEST(IndexedFilter, UpdateChild_AddedValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("ddd"); + Variant new_child(std::map{ + std::make_pair("eee", 200), + }); + Path affected_path; + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + std::make_pair("ddd", + std::map{ + std::make_pair("eee", 200), + }), + }); + ChildChangeAccumulator expected_changes{ + std::make_pair("ddd", ChildAddedChange("ddd", new_child)), + }; + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); +} + +TEST(IndexedFilter, UpdateChild_RemovedValue) { + QueryParams params; + IndexedFilter filter(params); + + Variant old_child(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + }), + }); + + IndexedVariant indexed_variant((Variant(old_child))); + std::string key("aaa"); + Variant new_child = Variant::Null(); + Path affected_path; + CompleteChildSource* source = nullptr; + ChildChangeAccumulator change_accumulator; + + IndexedVariant expected_result(Variant::EmptyMap()); + ChildChangeAccumulator expected_changes{ + std::make_pair( + "aaa", + ChildRemovedChange("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 100), + }), + })), + }; + EXPECT_EQ(filter.UpdateChild(indexed_variant, key, new_child, affected_path, + source, &change_accumulator), + expected_result); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); +} + +TEST(IndexedFilterDeathTest, UpdateChild_OrderByMismatch) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + IndexedFilter filter(params); + + IndexedVariant good_snap(Variant(), params); + IndexedVariant bad_snap; + + // Should be fine. + filter.UpdateChild(good_snap, "irrelevant_key", Variant("irrelevant variant"), + Path("irrelevant/path"), nullptr, nullptr); + + // Should die. + EXPECT_DEATH(filter.UpdateChild(bad_snap, "irrelevant_key", + Variant("irrelevant variant"), + Path("irrelevant/path"), nullptr, nullptr), + DEATHTEST_SIGABRT); +} + +TEST(IndexedFilter, UpdateFullVariant) { + { + QueryParams params; + IndexedFilter filter(params); + + IndexedVariant old_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 100), + std::make_pair("to_be_removed", 200), + std::make_pair("unchanged", 300), + }), + })); + IndexedVariant new_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 400), + std::make_pair("unchanged", 300), + std::make_pair("was_added", 500), + }), + })); + ChildChangeAccumulator change_accumulator; + + ChildChangeAccumulator expected_changes{ + std::make_pair("to_be_changed", + ChildChangedChange("to_be_changed", 400, 100)), + std::make_pair("to_be_removed", + ChildRemovedChange("to_be_removed", 200)), + std::make_pair("was_added", ChildAddedChange("was_added", 500)), + }; + + EXPECT_EQ(filter.UpdateFullVariant(old_snap, new_snap, &change_accumulator), + new_snap); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); + } + { + QueryParams params; + IndexedFilter filter(params); + + IndexedVariant old_snap(Variant(std::map{ + std::make_pair("to_be_changed", 100), + std::make_pair("to_be_removed", 200), + std::make_pair("unchanged", 300), + })); + IndexedVariant new_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 400), + std::make_pair("unchanged", 300), + std::make_pair("was_added", 500), + }), + })); + ChildChangeAccumulator change_accumulator; + + ChildChangeAccumulator expected_changes{ + std::make_pair("to_be_changed", + ChildChangedChange("to_be_changed", 400, 100)), + std::make_pair("to_be_removed", + ChildRemovedChange("to_be_removed", 200)), + std::make_pair("was_added", ChildAddedChange("was_added", 500)), + }; + + EXPECT_EQ(filter.UpdateFullVariant(old_snap, new_snap, &change_accumulator), + new_snap); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); + } + { + QueryParams params; + IndexedFilter filter(params); + + IndexedVariant old_snap(Variant(std::map{ + std::make_pair(".value", + std::map{ + std::make_pair("to_be_changed", 100), + std::make_pair("to_be_removed", 200), + std::make_pair("unchanged", 300), + }), + })); + IndexedVariant new_snap(Variant(std::map{ + std::make_pair("to_be_changed", 400), + std::make_pair("unchanged", 300), + std::make_pair("was_added", 500), + })); + ChildChangeAccumulator change_accumulator; + + ChildChangeAccumulator expected_changes{ + std::make_pair("to_be_changed", + ChildChangedChange("to_be_changed", 400, 100)), + std::make_pair("to_be_removed", + ChildRemovedChange("to_be_removed", 200)), + std::make_pair("was_added", ChildAddedChange("was_added", 500)), + }; + + EXPECT_EQ(filter.UpdateFullVariant(old_snap, new_snap, &change_accumulator), + new_snap); + EXPECT_THAT(change_accumulator, Pointwise(Eq(), expected_changes)); + } +} + +TEST(IndexedFilterDeathTest, UpdateFullVariant_OrderByMismatch) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + IndexedFilter filter(params); + + IndexedVariant irrelevant_snap; + IndexedVariant good_new_snap(Variant(), params); + IndexedVariant bad_new_snap; + + // Should not die. + filter.UpdateFullVariant(irrelevant_snap, good_new_snap, nullptr); + + // Should die. + EXPECT_DEATH(filter.UpdateFullVariant(irrelevant_snap, bad_new_snap, nullptr), + DEATHTEST_SIGABRT); +} + +TEST(IndexedFilter, UpdatePriority_Null) { + QueryParams params; + IndexedFilter filter(params); + IndexedVariant old_snap(Variant::Null()); + Variant new_priority = 100; + IndexedVariant result = filter.UpdatePriority(old_snap, new_priority); + EXPECT_EQ(result.variant(), Variant::Null()); +} + +TEST(IndexedFilter, UpdatePriority_FundamentalType) { + QueryParams params; + IndexedFilter filter(params); + IndexedVariant old_snap(Variant(100)); + Variant new_priority = "priority"; + IndexedVariant result = filter.UpdatePriority(old_snap, new_priority); + EXPECT_EQ(result.variant(), Variant(std::map{ + std::make_pair(".value", 100), + std::make_pair(".priority", "priority"), + })); +} + +TEST(IndexedFilter, UpdatePriority_Map) { + QueryParams params; + IndexedFilter filter(params); + IndexedVariant old_snap(Variant(std::map{ + std::make_pair("aaa", 111), + std::make_pair("bbb", 222), + std::make_pair("ccc", 333), + })); + Variant new_priority = "banana"; + IndexedVariant result = filter.UpdatePriority(old_snap, new_priority); + EXPECT_EQ(result.variant(), Variant(std::map{ + std::make_pair("aaa", 111), + std::make_pair("bbb", 222), + std::make_pair("ccc", 333), + std::make_pair(".priority", "banana"), + })); +} + +TEST(IndexedFilter, FiltersVariants) { + QueryParams params; + IndexedFilter filter(params); + EXPECT_FALSE(filter.FiltersVariants()); +} + +TEST(IndexedFilter, GetIndexedFilter) { + QueryParams params; + IndexedFilter filter(params); + EXPECT_EQ(filter.GetIndexedFilter(), &filter); +} + +TEST(IndexedFilter, query_spec) { + QueryParams params; + IndexedFilter filter(params); + EXPECT_EQ(filter.query_params(), params); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/limited_filter_test.cc b/database/tests/desktop/view/limited_filter_test.cc new file mode 100644 index 0000000000..ba81aae08a --- /dev/null +++ b/database/tests/desktop/view/limited_filter_test.cc @@ -0,0 +1,314 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/view/limited_filter.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(LimitedFilter, Constructor) { + { + QueryParams params; + params.limit_first = 2; + LimitedFilter filter(params); + } + { + QueryParams params; + params.limit_last = 2; + LimitedFilter filter(params); + } +} + +TEST(LimitedFilter, UpdateChildLimitFirst) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_first = 2; + LimitedFilter filter(params); + + Variant data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant old_snapshot(data, params); + + // Prepend new value. + { + IndexedVariant changed_result = + filter.UpdateChild(old_snapshot, "aaa", 100, Path(), nullptr, nullptr); + Variant expected_data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + }; + IndexedVariant expected_changed_result(expected_data, params); + EXPECT_EQ(changed_result, expected_changed_result); + } + + // New value at the end doesn't get appended. + { + IndexedVariant unchanged_result = + filter.UpdateChild(old_snapshot, "ddd", 400, Path(), nullptr, nullptr); + IndexedVariant expected_unchanged_result(data, params); + EXPECT_EQ(unchanged_result, expected_unchanged_result); + } +} + +TEST(LimitedFilter, UpdateChildLimitLast) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_last = 2; + LimitedFilter filter(params); + + Variant data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant old_snapshot(data, params); + + // New value at the beginning doesn't get prepending. + { + IndexedVariant unchanged_result = + filter.UpdateChild(old_snapshot, "aaa", 100, Path(), nullptr, nullptr); + IndexedVariant expected_unchanged_result(data, params); + EXPECT_EQ(unchanged_result, expected_unchanged_result); + } + + // Append new value. + { + IndexedVariant changed_result = + filter.UpdateChild(old_snapshot, "ddd", 400, Path(), nullptr, nullptr); + Variant expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_changed_result(expected_data, params); + EXPECT_EQ(changed_result, expected_changed_result); + } +} + +TEST(LimitedFilter, UpdateFullVariantLimitFirst) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_first = 2; + LimitedFilter filter(params); + + Variant old_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(old_data, params); + + // new_data removes elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data removes elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), std::make_pair("ccc", 300), + std::make_pair("ddd", 400), std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } +} + +TEST(LimitedFilter, UpdateFullVariantLimitLast) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.limit_last = 2; + LimitedFilter filter(params); + + Variant old_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(old_data, params); + + // new_data removes elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data removes elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the end. + { + Variant new_data = std::map{ + std::make_pair("bbb", 200), std::make_pair("ccc", 300), + std::make_pair("ddd", 400), std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } + + // new_data adds elements at the beginning. + { + Variant new_data = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant new_snapshot(new_data, params); + + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + Variant expected_data = std::map{ + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant expected_result(expected_data, params); + EXPECT_EQ(result, expected_result); + } +} + +TEST(LimitedFilter, UpdatePriority) { + QueryParams params; + params.limit_last = 2; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant priority = 9999; + IndexedVariant old_snapshot(data, params); + + // Same as old_snapshot. + IndexedVariant expected_value(data, params); + EXPECT_EQ(filter.UpdatePriority(old_snapshot, priority), expected_value); +} + +TEST(LimitedFilter, FiltersVariants) { + QueryParams params; + params.limit_last = 2; + LimitedFilter filter(params); + EXPECT_TRUE(filter.FiltersVariants()); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/ranged_filter_test.cc b/database/tests/desktop/view/ranged_filter_test.cc new file mode 100644 index 0000000000..174d710015 --- /dev/null +++ b/database/tests/desktop/view/ranged_filter_test.cc @@ -0,0 +1,673 @@ +// Copyright 2019 Google LLC +// +// 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 "database/src/desktop/view/ranged_filter.h" + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(RangedFilter, Constructor) { + // Test the assert condition in the RangedFilter. The filter must have one of + // the parameters set that affects the range of the query. + { + QueryParams params; + params.start_at_child_key = "the_beginning"; + RangedFilter filter(params); + } + { + QueryParams params; + params.start_at_value = Variant("the_beginning_value"); + RangedFilter filter(params); + } + { + QueryParams params; + params.end_at_child_key = "the_end"; + RangedFilter filter(params); + } + { + QueryParams params; + params.end_at_value = Variant("fin"); + RangedFilter filter(params); + } + { + QueryParams params; + params.equal_to_child_key = "specific_key"; + RangedFilter filter(params); + } + { + QueryParams params; + params.equal_to_value = Variant("specific_value"); + RangedFilter filter(params); + } +} + +TEST(RangedFilter, UpdateChildWithChildKeyFilter) { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "ccc"; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(data, params); + + // Add a new value that is outside of the range, which should not change + // the result. + IndexedVariant result = + filter.UpdateChild(old_snapshot, "aaa", 100, Path(), nullptr, nullptr); + + IndexedVariant expected_result(data, params); + EXPECT_EQ(result, expected_result); + + // Now add a new value that is inside the allowed range, and the result + // should update. + IndexedVariant new_result = + filter.UpdateChild(old_snapshot, "fff", 600, Path(), nullptr, nullptr); + + Variant new_expected_data = std::map{ + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + std::make_pair("eee", 500), + std::make_pair("fff", 600), + }; + IndexedVariant new_expected_result(new_expected_data, params); + + EXPECT_EQ(new_result, new_expected_result); +} + +TEST(RangedFilter, UpdateFullVariant) { + // Leaf + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "bbb"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + IndexedVariant old_snapshot(Variant::EmptyMap(), params); + IndexedVariant new_snapshot(1000, params); + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + EXPECT_EQ(result, IndexedVariant(Variant::Null(), params)); + } + + // Map + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "bbb"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("aaa", 100), std::make_pair("bbb", 200), + std::make_pair("ccc", 300), std::make_pair("ddd", 400), + std::make_pair("eee", 500), + }; + IndexedVariant old_snapshot(Variant::EmptyMap(), params); + IndexedVariant new_snapshot(data, params); + IndexedVariant result = + filter.UpdateFullVariant(old_snapshot, new_snapshot, nullptr); + + Variant expected_data = std::map{ + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + std::make_pair("ddd", 400), + }; + IndexedVariant expected_result(expected_data, params); + + EXPECT_EQ(result, expected_result); + } +} + +TEST(RangedFilter, UpdatePriority) { + QueryParams params; + params.start_at_child_key = "aaa"; + RangedFilter filter(params); + + Variant data = std::map{ + std::make_pair("aaa", 100), + std::make_pair("bbb", 200), + std::make_pair("ccc", 300), + }; + Variant priority = 9999; + IndexedVariant old_snapshot(data, params); + + // Same as old_snapshot. + IndexedVariant expected_value(data, params); + EXPECT_EQ(filter.UpdatePriority(old_snapshot, priority), expected_value); +} + +TEST(RangedFilter, FiltersVariants) { + QueryParams params; + params.start_at_child_key = "aaa"; + RangedFilter filter(params); + EXPECT_TRUE(filter.FiltersVariants()); +} + +TEST(RangedFilter, StartAndEndPost) { + // Priority + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = std::make_pair( + "aaa", std::map{std::make_pair(".priority", "bbb")}); + std::pair expected_end_post = std::make_pair( + "ccc", std::map{std::make_pair(".priority", "ddd")}); + + EXPECT_EQ(start_post, expected_start_post); + EXPECT_EQ(end_post, expected_end_post); + } + + // Child + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + params.order_by_child = "zzz"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = std::make_pair( + "aaa", std::map{std::make_pair("zzz", "bbb")}); + std::pair expected_end_post = std::make_pair( + "ccc", std::map{std::make_pair("zzz", "ddd")}); + + EXPECT_EQ(start_post, expected_start_post) + << util::VariantToJson(start_post.first) << " | " + << util::VariantToJson(start_post.second); + EXPECT_EQ(end_post, expected_end_post) + << util::VariantToJson(end_post.first) << " | " + << util::VariantToJson(end_post.second); + } + + // Key + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = + std::make_pair("bbb", Variant::Null()); + std::pair expected_end_post = + std::make_pair("ddd", Variant::Null()); + + EXPECT_EQ(start_post, expected_start_post); + EXPECT_EQ(end_post, expected_end_post); + } + + // Value + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.start_at_child_key = "aaa"; + params.start_at_value = "bbb"; + params.end_at_child_key = "ccc"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + std::pair start_post = filter.start_post(); + std::pair end_post = filter.end_post(); + std::pair expected_start_post = + std::make_pair("aaa", "bbb"); + std::pair expected_end_post = + std::make_pair("ccc", "ddd"); + + EXPECT_EQ(start_post, expected_start_post); + EXPECT_EQ(end_post, expected_end_post); + } +} + +TEST(RangedFilter, MatchesByPriority) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.start_at_child_key = "ccc"; + params.start_at_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.end_at_child_key = "ccc"; + params.end_at_value = 300; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_TRUE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.start_at_child_key = "bbb"; + params.start_at_value = 200; + params.end_at_child_key = "ddd"; + params.end_at_value = 400; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByPriority; + params.equal_to_child_key = "ccc"; + params.equal_to_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 200))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ccc", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", std::map{std::make_pair(".priority", 100)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 200)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", std::map{std::make_pair(".priority", 300)}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 300)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", std::map{std::make_pair(".priority", 400)}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", std::map{std::make_pair(".priority", 500)}))); + } +} + +TEST(RangedFilter, MatchesByChild) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.start_at_child_key = "ccc"; + params.start_at_value = 300; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.end_at_child_key = "ccc"; + params.end_at_value = 300; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.start_at_child_key = "bbb"; + params.start_at_value = 200; + params.end_at_child_key = "ddd"; + params.end_at_value = 400; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.equal_to_child_key = "ccc"; + params.equal_to_value = 300; + params.order_by_child = "zzz/yyy"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "aaa", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 100)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "bbb", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 200)})}))); + EXPECT_TRUE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 300)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 400)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "ddd", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + EXPECT_FALSE(filter.Matches(std::make_pair( + "eee", + std::map{std::make_pair( + "zzz", std::map{std::make_pair("yyy", 500)})}))); + + EXPECT_FALSE(filter.Matches(std::make_pair( + "ccc", + std::map{std::make_pair("zzz", Variant::Null())}))); + } +} + +TEST(RangedFilter, MatchesByKey) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "ccc"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("eee", 500))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.end_at_value = "ccc"; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.start_at_value = "bbb"; + params.end_at_value = "ddd"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByKey; + params.equal_to_value = "ccc"; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } +} + +TEST(RangedFilter, MatchesByValue) { + // StartAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.start_at_child_key = "ccc"; + params.start_at_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_TRUE(filter.Matches(std::make_pair("eee", 500))); + } + + // EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.end_at_child_key = "ccc"; + params.end_at_value = 300; + RangedFilter filter(params); + + EXPECT_TRUE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // StartAt and EndAt + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.start_at_child_key = "bbb"; + params.start_at_value = 200; + params.end_at_child_key = "ddd"; + params.end_at_value = 400; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_TRUE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_TRUE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } + + // EqualTo + { + QueryParams params; + params.order_by = QueryParams::kOrderByValue; + params.equal_to_child_key = "ccc"; + params.equal_to_value = 300; + RangedFilter filter(params); + + EXPECT_FALSE(filter.Matches(std::make_pair("aaa", 100))); + EXPECT_FALSE(filter.Matches(std::make_pair("bbb", 200))); + EXPECT_TRUE(filter.Matches(std::make_pair("ccc", 300))); + EXPECT_FALSE(filter.Matches(std::make_pair("ddd", 400))); + EXPECT_FALSE(filter.Matches(std::make_pair("eee", 500))); + } +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/view_cache_test.cc b/database/tests/desktop/view/view_cache_test.cc new file mode 100644 index 0000000000..ec837a5c96 --- /dev/null +++ b/database/tests/desktop/view/view_cache_test.cc @@ -0,0 +1,133 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/view_cache.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(ViewCacheTest, Constructors) { + // Everything should be uninitialized. + ViewCache blank_cache; + // Local + EXPECT_EQ(blank_cache.local_snap().variant(), Variant::Null()); + EXPECT_FALSE(blank_cache.local_snap().fully_initialized()); + EXPECT_FALSE(blank_cache.local_snap().filtered()); + // Server + EXPECT_EQ(blank_cache.server_snap().variant(), Variant::Null()); + EXPECT_FALSE(blank_cache.server_snap().fully_initialized()); + EXPECT_FALSE(blank_cache.server_snap().filtered()); + + CacheNode local_cache(IndexedVariant("local_value"), true, false); + CacheNode server_cache(IndexedVariant("server_value"), false, true); + ViewCache populated_cache(local_cache, server_cache); + // Local + EXPECT_EQ(populated_cache.local_snap().variant(), "local_value"); + EXPECT_TRUE(populated_cache.local_snap().fully_initialized()); + EXPECT_FALSE(populated_cache.local_snap().filtered()); + // Server + EXPECT_EQ(populated_cache.server_snap().variant(), "server_value"); + EXPECT_FALSE(populated_cache.server_snap().fully_initialized()); + EXPECT_TRUE(populated_cache.server_snap().filtered()); +} + +TEST(ViewCacheTest, GetCompleteSnaps) { + // Everything should be uninitialized. + ViewCache blank_cache; + EXPECT_EQ(blank_cache.GetCompleteLocalSnap(), nullptr); + EXPECT_EQ(blank_cache.GetCompleteServerSnap(), nullptr); + + // Initialize the local and server cache. + CacheNode local_cache(IndexedVariant("local_value"), true, true); + CacheNode server_cache(IndexedVariant("server_value"), true, true); + ViewCache populated_cache(local_cache, server_cache); + EXPECT_EQ(populated_cache.GetCompleteLocalSnap(), + &populated_cache.local_snap().variant()); + EXPECT_EQ(populated_cache.GetCompleteServerSnap(), + &populated_cache.server_snap().variant()); +} + +TEST(ViewCacheTest, UpdateLocalSnap) { + // Start uninitialized and update the local cache. + ViewCache view_cache; + ViewCache local_update = + view_cache.UpdateLocalSnap(IndexedVariant("local_value"), true, true); + // Local + EXPECT_STREQ(local_update.local_snap().variant().string_value(), + "local_value"); + EXPECT_TRUE(local_update.local_snap().fully_initialized()); + EXPECT_TRUE(local_update.local_snap().filtered()); + // Server (should be unchanged). + EXPECT_TRUE(local_update.server_snap().variant().is_null()); + EXPECT_FALSE(local_update.server_snap().fully_initialized()); + EXPECT_FALSE(local_update.server_snap().filtered()); +} + +TEST(ViewCacheTest, UpdateServerSnap) { + // Start uninitialized and update the server cache. + ViewCache view_cache; + ViewCache server_update = + view_cache.UpdateServerSnap(IndexedVariant("server_value"), true, true); + // Local (should be unchanged). + EXPECT_TRUE(server_update.local_snap().variant().is_null()); + EXPECT_FALSE(server_update.local_snap().fully_initialized()); + EXPECT_FALSE(server_update.local_snap().filtered()); + // Server + EXPECT_STREQ(server_update.server_snap().variant().string_value(), + "server_value"); + EXPECT_TRUE(server_update.server_snap().fully_initialized()); + EXPECT_TRUE(server_update.server_snap().filtered()); +} + +TEST(ViewCacheTest, CacheNodeEquality) { + CacheNode cache_node(IndexedVariant("some_string"), true, true); + CacheNode same_cache_node(IndexedVariant("some_string"), true, true); + CacheNode different_variant(IndexedVariant("different_string"), true, true); + CacheNode different_fully_initialized(IndexedVariant("some_string"), false, + true); + CacheNode different_filtered(IndexedVariant("some_string"), true, false); + + EXPECT_EQ(cache_node, same_cache_node); + EXPECT_NE(cache_node, different_variant); + EXPECT_NE(cache_node, different_fully_initialized); + EXPECT_NE(cache_node, different_filtered); +} + +TEST(ViewCacheTest, ViewCacheEquality) { + CacheNode local_cache(IndexedVariant("local_value"), true, true); + CacheNode server_cache(IndexedVariant("server_value"), true, true); + ViewCache view_cache(local_cache, server_cache); + ViewCache same_view_cache(local_cache, server_cache); + + CacheNode different_local_cache_node(IndexedVariant("wrong_local_value"), + true, true); + CacheNode different_server_cache_node(IndexedVariant("server_value"), false, + true); + ViewCache different_local_cache(different_local_cache_node, server_cache); + ViewCache different_server_cache(local_cache, different_server_cache_node); + + EXPECT_EQ(view_cache, same_view_cache); + EXPECT_NE(view_cache, different_local_cache); + EXPECT_NE(view_cache, different_server_cache); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/view_processor_test.cc b/database/tests/desktop/view/view_processor_test.cc new file mode 100644 index 0000000000..296ca3c31e --- /dev/null +++ b/database/tests/desktop/view/view_processor_test.cc @@ -0,0 +1,727 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/view_processor.h" + +#include "app/src/variant_util.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "database/src/desktop/core/indexed_variant.h" +#include "database/src/desktop/core/operation.h" +#include "database/src/desktop/util_desktop.h" +#include "database/src/desktop/view/indexed_filter.h" + +// There are four types of operations we can apply: Overwrites, Merges, +// AckUserWrites, and ListenCompletes. Overwrites and merges can come from +// either the client or the server. AckUserWrites and ListenCompletes only come +// from the server. A test has been written for each combination of Operation +// type and operation source, and in the cases where there are significantly +// diverging code paths within a given conbination, multiple tests have been +// written to test each code path. + +using ::testing::Eq; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(ViewProcessor, Constructor) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // No tests, just making sure the indexed filter doesn't leak after + // destruction. +} + +// Apply an Overwrite operation that was initiated by the user, using an empty +// path. +TEST(ViewProcessor, ApplyOperationUserOverwrite_WithEmptyPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a user-initiated overwrite with an empty path to change a value. + Operation operation = + Operation::Overwrite(OperationSource::kUser, Path(), Variant("apples")); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache(Variant("apples"), true, false); + ViewCache expected_view_cache(expected_local_cache, initial_server_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(Variant("apples"))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the user, using a +// .priority path. +TEST(ViewProcessor, ApplyOperationUserOverwrite_WithPriorityPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a user-initiated overwrite with an empty path to change a value. + Operation operation = Operation::Overwrite(OperationSource::kUser, + Path(".priority"), Variant(100)); + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache(CombineValueAndPriority("local_values", 100), + true, false); + ViewCache expected_view_cache(expected_local_cache, initial_server_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(CombineValueAndPriority("local_values", 100))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the user, regular +// non-empty path. +TEST(ViewProcessor, ApplyOperationUserOverwrite_WithRegularPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a user-initiated overwrite with a non-empty path to change a value. + Operation operation = Operation::Overwrite( + OperationSource::kUser, Path("aaa/bbb"), Variant("apples")); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path("aaa/bbb")); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }), + true, false); + ViewCache expected_view_cache(expected_local_cache, initial_server_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildAddedChange("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the server, using an empty +// path. +TEST(ViewProcessor, ApplyOperationServerOverwrite_WithEmptyPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a server-initiated overwrite with an empty path to change a value. + Operation operation = + Operation::Overwrite(OperationSource::kServer, Path(), Variant("apples")); + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both the local and server caches have been set. + CacheNode expected_cache(Variant("apples"), true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(Variant("apples"))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the server, using a +// regular path. +TEST(ViewProcessor, ApplyOperationServerOverwrite_RegularPath) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + Operation operation = + Operation::Overwrite(OperationSource::kServer, Path("aaa"), + Variant(std::map{ + std::make_pair("bbb", "apples"), + })); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both caches are expected to be the same. + CacheNode expected_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }), + true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect one ChildAdded event and one Value event. + std::vector expected_changes{ + ChildAddedChange("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "apples"), + }), + }))), + }; + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply an Overwrite operation that was initiated by the server, using a path +// that is deeper than a direct child of the location. +TEST(ViewProcessor, ApplyOperationServerOverwrite_DistantDescendantChange) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_cache(Variant(std::map{std::make_pair( + "aaa", + std::map{std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", 1000), + })})}), + true, false); + ViewCache old_view_cache(initial_cache, initial_cache); + + // Make sure the data being updated is deeply nested in the variant. + Operation operation = Operation::Overwrite( + OperationSource::kServer, Path("aaa/bbb/ccc"), Variant(-9999)); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both caches are expected to be the same. + CacheNode expected_cache(Variant(std::map{std::make_pair( + "aaa", + std::map{std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", -9999), + })})}), + true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildChangedChange("aaa", + IndexedVariant(Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", -9999), + })})), + IndexedVariant(Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", 1000), + })}))), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", -9999), + })})}))), + }; + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +// Apply a Merge operation that was initiated by the user. +TEST(ViewProcessor, ApplyOperationUserMerge) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "zzz"), + }), + }), + true, false); + CacheNode initial_server_cache(Variant("aaa"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + // The merge operation should consist of multiple changes in different + // locations. + CompoundWrite write = + CompoundWrite() + .AddWrite(Path("aaa/bbb/ccc"), Variant("apples")) + .AddWrite(Path("aaa/ddd"), Variant("bananas")) + .AddWrite(Path("aaa/eee/fff"), Variant("vegetables")); + Operation operation = Operation::Merge(OperationSource::kUser, Path(), write); + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Only the local cache should change. + CacheNode expected_local_cache( + Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", "apples"), + }), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + true, false); + CacheNode expected_server_cache(Variant("aaa"), true, false); + ViewCache expected_view_cache(expected_local_cache, expected_server_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildChangedChange( + "aaa", + Variant(std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", "apples"), + }), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + Variant(std::map{ + std::make_pair("bbb", "zzz"), + })), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair("bbb", + std::map{ + std::make_pair("ccc", "apples"), + }), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationServerMerge) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "zzz"), + }), + }), + true, false); + CacheNode initial_server_cache(Variant("aaa"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // The merge operation should consist of multiple changes in different + // locations. + CompoundWrite write = + CompoundWrite() + .AddWrite(Path("bbb/ccc"), Variant("apples")) + .AddWrite(Path("bbb/ddd"), Variant("bananas")) + .AddWrite(Path("bbb/eee/fff"), Variant("vegetables")); + Operation operation = + Operation::Merge(OperationSource::kServer, Path("aaa"), write); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path("aaa")); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Both caches are expected to be the same. + CacheNode expected_cache( + Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", "apples"), + std::make_pair("ddd", "bananas"), + std::make_pair( + "eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + }), + true, false); + ViewCache expected_view_cache(expected_cache, expected_cache); + + // Expect one ChildChanged event and one Value event. + std::vector expected_changes{ + ChildChangedChange( + "aaa", + Variant(std::map{ + std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", "apples"), + std::make_pair("ddd", "bananas"), + std::make_pair("eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + Variant(std::map{ + std::make_pair("bbb", "zzz"), + })), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair( + "aaa", + std::map{ + std::make_pair( + "bbb", + std::map{ + std::make_pair("ccc", "apples"), + std::make_pair("ddd", "bananas"), + std::make_pair( + "eee", + std::map{ + std::make_pair("fff", "vegetables"), + }), + }), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationAck_HasShadowingWrite) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create an Ack with a shadowing write. + // These values don't matter for this test because the shadowing write will + // short circuit everything. + Tree affected_tree; + Operation operation = + Operation::AckUserWrite(Path("aaa"), affected_tree, kAckConfirm); + + // Set up shadowing write. + WriteTree writes_cache; + writes_cache.AddOverwrite(Path("aaa"), Variant("overwrite"), 100, + kOverwriteVisible); + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Expect no changes in the view cache. + ViewCache expected_view_cache = old_view_cache; + + // Expect no Changes as a result of this. + std::vector expected_changes; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationAck_IsOverwrite) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "new_value"), + }), + }), + true, false); + CacheNode initial_server_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "new_value"), + }), + }), + true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + Tree affected_tree; + affected_tree.set_value(true); + affected_tree.SetValueAt(Path("aaa/bbb"), true); + Operation operation = + Operation::AckUserWrite(Path(), affected_tree, kAckConfirm); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + writes_cache.AddOverwrite(Path("aaa/bbb"), "new_value", 1234, + kOverwriteVisible); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Expect no changes in the view cache. + ViewCache expected_view_cache(initial_local_cache, initial_server_cache); + + // Expect no Changes as a result of this. + std::vector expected_changes; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationAckRevert) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "new_value"), + }), + }), + true, false); + CacheNode initial_server_cache( + Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "old_value"), + }), + }), + true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Mark the value we're going to be reverting. + Tree affected_tree; + affected_tree.set_value(true); + affected_tree.SetValueAt(Path("aaa/bbb"), true); + Operation operation = + Operation::AckUserWrite(Path(), affected_tree, kAckRevert); + + // Hold the old value in the writes cache. + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + writes_cache.AddOverwrite(Path("aaa/bbb"), "old_value", 1234, + kOverwriteVisible); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // Expect that the local cache gets reverted to the old value. + ViewCache expected_view_cache(initial_server_cache, initial_server_cache); + + // Expect a ChildChanged and Value Changes, setting things back to the old + // value. + std::vector expected_changes{ + ChildChangedChange("aaa", + Variant(std::map{ + std::make_pair("bbb", "old_value"), + }), + Variant(std::map{ + std::make_pair("bbb", "new_value"), + })), + ValueChange(IndexedVariant(Variant(std::map{ + std::make_pair("aaa", + std::map{ + std::make_pair("bbb", "old_value"), + }), + }))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +TEST(ViewProcessor, ApplyOperationListenComplete) { + ViewProcessor view_processor(MakeUnique(QueryParams())); + + // Set up some dummy data. + CacheNode initial_local_cache(Variant("local_values"), true, false); + CacheNode initial_server_cache(Variant("server_values"), true, false); + ViewCache old_view_cache(initial_local_cache, initial_server_cache); + + // Create a server-initiated listen complete with an empty path to change a + // value. + Operation operation = + Operation::ListenComplete(OperationSource::kServer, Path()); + + WriteTree writes_cache; + WriteTreeRef writes_cache_ref = writes_cache.ChildWrites(Path()); + Variant complete_cache; + + // Apply the operation. + ViewCache resultant_view_cache; + std::vector resultant_changes; + view_processor.ApplyOperation(old_view_cache, operation, writes_cache_ref, + &complete_cache, &resultant_view_cache, + &resultant_changes); + + // The local cache should now reflect the server cache. + ViewCache expected_view_cache(initial_server_cache, initial_server_cache); + + // Expect just a value change event. + std::vector expected_changes{ + ValueChange(IndexedVariant(Variant("server_values"))), + }; + + EXPECT_EQ(resultant_view_cache, expected_view_cache); + EXPECT_THAT(resultant_changes, Pointwise(Eq(), expected_changes)); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/database/tests/desktop/view/view_test.cc b/database/tests/desktop/view/view_test.cc new file mode 100644 index 0000000000..fab8dc4eb3 --- /dev/null +++ b/database/tests/desktop/view/view_test.cc @@ -0,0 +1,464 @@ +// Copyright 2018 Google LLC +// +// 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 "database/src/desktop/view/view.h" + +#include "app/memory/unique_ptr.h" +#include "app/src/variant_util.h" +#include "database/src/desktop/core/event_registration.h" +#include "database/src/desktop/core/value_event_registration.h" +#include "database/src/desktop/core/write_tree.h" +#include "database/src/desktop/data_snapshot_desktop.h" +#include "database/src/desktop/view/view_cache.h" +#include "database/src/include/firebase/database/common.h" +#include "database/tests/desktop/test/matchers.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::Eq; +using ::testing::Not; +using ::testing::Pointwise; + +namespace firebase { +namespace database { +namespace internal { +namespace { + +TEST(View, Constructor) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode local_cache(IndexedVariant(Variant(), query_spec.params), true, + true); + CacheNode server_cache(IndexedVariant(Variant(), query_spec.params), true, + false); + ViewCache initial_view_cache(local_cache, server_cache); + + View view(query_spec, initial_view_cache); + + EXPECT_EQ(view.query_spec(), query_spec); + EXPECT_EQ(view.view_cache(), initial_view_cache); +} + +TEST(View, MoveConstructor) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), query_spec.params), true, + false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + View new_view(std::move(old_view)); + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +TEST(View, MoveAssignment) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), params), true, false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + // When we move the old_view into the new_view, make sure any existing + // registrations are properly cleaned up and not leaked. + ValueEventRegistration* registration_to_be_deleted = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + View new_view((QuerySpec()), ViewCache(CacheNode(), CacheNode())); + new_view.AddEventRegistration( + UniquePtr(registration_to_be_deleted)); + + new_view = std::move(old_view); + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +// For Views, copies are actually moves, so this test is identical to the +// MoveConstructor test. +TEST(View, CopyConstructor) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), params), true, false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + View new_view(old_view); + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +// For Views, copies are actually moves, so this test is identical to the +// MoveAssignment test. +TEST(View, CopyAssignment) { + QueryParams params; + params.order_by = QueryParams::kOrderByChild; + params.order_by_child = "order_by_child"; + QuerySpec query_spec(Path("test/path"), params); + CacheNode cache(IndexedVariant(Variant("test"), params), true, false); + ViewCache initial_view_cache(cache, cache); + + View old_view(query_spec, initial_view_cache); + + // Add an event registration to make sure that it gets moved to the new View. + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + // old_view now owns registration. + old_view.AddEventRegistration( + UniquePtr(registration)); + + // When we move the old_view into the new_view, make sure any existing + // registrations are properly cleaned up and not leaked. + ValueEventRegistration* registration_to_be_deleted = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + View new_view((QuerySpec()), ViewCache(CacheNode(), CacheNode())); + new_view.AddEventRegistration( + UniquePtr(registration_to_be_deleted)); + + new_view = old_view; + + // The old cache should have its event registrations cleared out. If the + // registration was left behind in the old_view, this test will crash at the + // end due to double-deleting the registration. + + // The new cache should be exactly what the old one was. + EXPECT_EQ(new_view.query_spec(), query_spec); + EXPECT_EQ(new_view.view_cache(), initial_view_cache); + EXPECT_THAT(new_view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), {registration})); +} + +TEST(View, GetCompleteServerCache_Empty) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(), query_spec.params), true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + EXPECT_EQ(view.GetCompleteServerCache(Path("test/path")), nullptr); +} + +TEST(View, GetCompleteServerCache_NonEmpty) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(std::map{ + std::make_pair("foo", "bar"), + std::make_pair("baz", "quux"), + }), + query_spec.params), + true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + EXPECT_EQ(*view.GetCompleteServerCache(Path("foo")), "bar"); +} + +TEST(View, IsNotEmpty) { + QuerySpec query_spec; + ViewCache initial_view_cache; + View view(query_spec, initial_view_cache); + + ValueEventRegistration* registration = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration)); + + EXPECT_FALSE(view.IsEmpty()); +} + +TEST(View, IsEmpty) { + QuerySpec query_spec; + ViewCache initial_view_cache; + View view(query_spec, initial_view_cache); + + EXPECT_TRUE(view.IsEmpty()); +} + +TEST(View, AddEventRegistration) { + QuerySpec query_spec; + ViewCache initial_view_cache; + View view(query_spec, initial_view_cache); + + ValueEventRegistration* registration1 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + ValueEventRegistration* registration2 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + ValueEventRegistration* registration3 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + ValueEventRegistration* registration4 = + new ValueEventRegistration(nullptr, nullptr, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration1)); + view.AddEventRegistration(UniquePtr(registration2)); + view.AddEventRegistration(UniquePtr(registration3)); + view.AddEventRegistration(UniquePtr(registration4)); + + std::vector expected_registrations{ + registration1, + registration2, + registration3, + registration4, + }; + + EXPECT_THAT(view.event_registrations(), + Pointwise(SmartPtrRawPtrEq(), expected_registrations)); +} + +class DummyValueListener : public ValueListener { + public: + ~DummyValueListener() override {} + void OnValueChanged(const DataSnapshot& snapshot) override {} + void OnCancelled(const Error& error, const char* error_message) override {} +}; + +TEST(View, RemoveEventRegistration_RemoveOne) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(), query_spec.params), true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + DummyValueListener listener1; + DummyValueListener listener2; + DummyValueListener listener3; + DummyValueListener listener4; + + ValueEventRegistration* registration1 = + new ValueEventRegistration(nullptr, &listener1, QuerySpec()); + ValueEventRegistration* registration2 = + new ValueEventRegistration(nullptr, &listener2, QuerySpec()); + ValueEventRegistration* registration3 = + new ValueEventRegistration(nullptr, &listener3, QuerySpec()); + ValueEventRegistration* registration4 = + new ValueEventRegistration(nullptr, &listener4, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration1)); + view.AddEventRegistration(UniquePtr(registration2)); + view.AddEventRegistration(UniquePtr(registration3)); + view.AddEventRegistration(UniquePtr(registration4)); + + std::vector expected_events{}; + std::vector expected_registrations{ + registration1, + registration2, + registration4, + }; + + EXPECT_THAT( + view.RemoveEventRegistration(static_cast(&listener3), kErrorNone), + Pointwise(Eq(), expected_events)); +} + +TEST(View, RemoveEventRegistration_RemoveAll) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + query_spec.params.start_at_value = "Apple"; + CacheNode cache(IndexedVariant(Variant(), query_spec.params), true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + DummyValueListener listener1; + DummyValueListener listener2; + DummyValueListener listener3; + DummyValueListener listener4; + + ValueEventRegistration* registration1 = + new ValueEventRegistration(nullptr, &listener1, QuerySpec()); + ValueEventRegistration* registration2 = + new ValueEventRegistration(nullptr, &listener2, QuerySpec()); + ValueEventRegistration* registration3 = + new ValueEventRegistration(nullptr, &listener3, QuerySpec()); + ValueEventRegistration* registration4 = + new ValueEventRegistration(nullptr, &listener4, QuerySpec()); + view.AddEventRegistration(UniquePtr(registration1)); + view.AddEventRegistration(UniquePtr(registration2)); + view.AddEventRegistration(UniquePtr(registration3)); + view.AddEventRegistration(UniquePtr(registration4)); + + std::vector results = + view.RemoveEventRegistration(nullptr, kErrorDisconnected); + + EXPECT_EQ(results.size(), 4); + + EXPECT_EQ(results[0].type, kEventTypeError); + EXPECT_EQ(results[0].event_registration, registration1); + EXPECT_EQ(results[0].error, kErrorDisconnected); + EXPECT_EQ(results[0].path, Path("test/path")); + EXPECT_EQ(results[0].event_registration_ownership_ptr.get(), registration1); + + EXPECT_EQ(results[1].type, kEventTypeError); + EXPECT_EQ(results[1].event_registration, registration2); + EXPECT_EQ(results[1].error, kErrorDisconnected); + EXPECT_EQ(results[1].path, Path("test/path")); + EXPECT_EQ(results[1].event_registration_ownership_ptr.get(), registration2); + + EXPECT_EQ(results[2].type, kEventTypeError); + EXPECT_EQ(results[2].event_registration, registration3); + EXPECT_EQ(results[2].error, kErrorDisconnected); + EXPECT_EQ(results[2].path, Path("test/path")); + EXPECT_EQ(results[2].event_registration_ownership_ptr.get(), registration3); + + EXPECT_EQ(results[3].type, kEventTypeError); + EXPECT_EQ(results[3].event_registration, registration4); + EXPECT_EQ(results[3].error, kErrorDisconnected); + EXPECT_EQ(results[3].path, Path("test/path")); + EXPECT_EQ(results[3].event_registration_ownership_ptr.get(), registration4); +} + +// View::ApplyOperation tests omitted. It just calls through to the functions +// ViewProcessor::ApplyOperation and GenerateEventsForChanges, and it is +// difficult to mock the interaction. Those functions are themselves tested in +// view_processor_test.cc and event_generator_test.cc respectively. + +TEST(ViewDeathTest, ApplyOperation_MustHaveLocalCache) { + QuerySpec query_spec; + CacheNode local_cache(IndexedVariant(Variant()), true, false); + CacheNode server_cache(IndexedVariant(Variant()), false, false); + ViewCache initial_view_cache(local_cache, server_cache); + View view(query_spec, initial_view_cache); + + Operation operation(Operation::kTypeMerge, + OperationSource(Optional()), Path(), + Variant(), CompoundWrite(), Tree(), kAckConfirm); + WriteTree write_tree; + WriteTreeRef writes_cache(Path(), &write_tree); + Variant complete_server_cache; + std::vector changes; + + EXPECT_DEATH(view.ApplyOperation(operation, writes_cache, + &complete_server_cache, &changes), + DEATHTEST_SIGABRT); +} + +TEST(ViewDeathTest, ApplyOperation_MustHaveServerCache) { + QuerySpec query_spec; + CacheNode local_cache(IndexedVariant(Variant()), false, false); + CacheNode server_cache(IndexedVariant(Variant()), true, false); + ViewCache initial_view_cache(local_cache, server_cache); + View view(query_spec, initial_view_cache); + + Operation operation(Operation::kTypeMerge, + OperationSource(Optional()), Path(), + Variant(), CompoundWrite(), Tree(), kAckConfirm); + WriteTree write_tree; + WriteTreeRef writes_cache(Path(), &write_tree); + Variant complete_server_cache; + std::vector changes; + + EXPECT_DEATH(view.ApplyOperation(operation, writes_cache, + &complete_server_cache, &changes), + DEATHTEST_SIGABRT); +} + +TEST(View, GetInitialEvents) { + QuerySpec query_spec; + query_spec.path = Path("test/path"); + query_spec.params.order_by = QueryParams::kOrderByValue; + CacheNode cache(IndexedVariant(Variant(std::map{ + std::make_pair("foo", "bar"), + std::make_pair("baz", "quux"), + }), + query_spec.params), + true, false); + ViewCache initial_view_cache(cache, cache); + View view(query_spec, initial_view_cache); + + ValueEventRegistration registration(nullptr, nullptr, QuerySpec()); + + std::vector results = view.GetInitialEvents(®istration); + std::vector expected_results{ + Event(kEventTypeValue, ®istration, + DataSnapshotInternal(nullptr, + Variant(std::map{ + std::make_pair("foo", "bar"), + std::make_pair("baz", "quux"), + }), + query_spec), + ""), + }; + + EXPECT_THAT(results, Pointwise(Eq(), expected_results)); +} + +TEST(View, GetEventCache) { + CacheNode local_cache(IndexedVariant(Variant("Apples")), false, false); + CacheNode server_cache(IndexedVariant(Variant("Bananas")), true, false); + ViewCache initial_view_cache(local_cache, server_cache); + View view(QuerySpec(), initial_view_cache); + + EXPECT_EQ(view.GetLocalCache(), "Apples"); +} + +} // namespace +} // namespace internal +} // namespace database +} // namespace firebase diff --git a/firestore/generate_android_test.py b/firestore/generate_android_test.py new file mode 100755 index 0000000000..83c039d04d --- /dev/null +++ b/firestore/generate_android_test.py @@ -0,0 +1,88 @@ +#!/usr/grte/v4/bin/python2.7 +"""Generate JUnit4 tests from gtest files. + +This script reads a template and fills in test-specific information such as .so +library name and Java class name. This script also goes over the gtest files and +finds all test methods of the pattern TEST_F(..., ...) and converts each into a +@Test-annotated test method. +""" + +# We will be open-source this. So please do not introduce google3 dependency +# unless absolutely necessary. + +import argparse +import re + +GTEST_METHOD_RE = (r'TEST_F[(]\s*(?P[A-Za-z]+)\s*,\s*' + r'(?P[A-Za-z]+)\s*[)]') + +JAVA_TEST_METHOD = r""" + @Test + public void {test_class}{test_method}() {{ + run("{test_class}.{test_method}"); + }} +""" + + +def generate_fragment(gtests): + """Generate @Test-annotated test method code from the provided gtest files.""" + fragments = [] + gtest_method_pattern = re.compile(GTEST_METHOD_RE) + for gtest in gtests: + with open(gtest, 'r') as gtest_file: + gtest_code = gtest_file.read() + for matched in re.finditer(gtest_method_pattern, gtest_code): + fragments.append( + JAVA_TEST_METHOD.format( + test_class=matched.group('test_class'), + test_method=matched.group('test_method'))) + return ''.join(fragments) + + +def generate_file(template, out, **kwargs): + """Generate a Java file from the provided template and parameters.""" + with open(template, 'r') as template_file: + template_string = template_file.read() + java_code = template_string.format(**kwargs) + with open(out, 'w') as out_file: + out_file.write(java_code) + + +def main(): + parser = argparse.ArgumentParser( + description='Generates JUnit4 tests from gtest files.') + parser.add_argument( + '--template', + help='the filename of the template to use in the generation', + required=True) + parser.add_argument( + '--java_package', + help='which package test Java class belongs to', + required=True) + parser.add_argument( + '--java_class', + help='specifies the name of the class to generate', + required=True) + parser.add_argument( + '--so_lib', + help=('specifies the name of the native library without prefix lib and ' + 'suffix .so. You must compile the C++ test code together with the ' + 'firestore_android_test_main.cc as a shared library, say libfoo.so ' + 'and pass the name foo here.'), + required=True) + parser.add_argument('--out', help='the output file path', required=True) + parser.add_argument('srcs', nargs='+', help='the input gtest file paths') + args = parser.parse_args() + + fragment = generate_fragment(args.srcs) + generate_file( + args.template, + args.out, + package_name=args.java_package, + java_class_name=args.java_class, + so_lib_name=args.so_lib, + tests=fragment) + + +if __name__ == '__main__': + main() diff --git a/firestore/src/tests/android/field_path_portable_test.cc b/firestore/src/tests/android/field_path_portable_test.cc new file mode 100644 index 0000000000..925bbd7eb0 --- /dev/null +++ b/firestore/src/tests/android/field_path_portable_test.cc @@ -0,0 +1,140 @@ +#include "firestore/src/android/field_path_portable.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +// The test cases are copied from +// Firestore/core/test/firebase/firestore/model/field_path_test.cc + +TEST(FieldPathPortableTest, Indexing) { + const FieldPathPortable path({"rooms", "Eros", "messages"}); + + EXPECT_EQ(path[0], "rooms"); + EXPECT_EQ(path[1], "Eros"); + EXPECT_EQ(path[2], "messages"); +} + +TEST(FieldPathPortableTest, Comparison) { + const FieldPathPortable abc({"a", "b", "c"}); + const FieldPathPortable abc2({"a", "b", "c"}); + const FieldPathPortable xyz({"x", "y", "z"}); + EXPECT_EQ(abc, abc2); + EXPECT_NE(abc, xyz); + + const FieldPathPortable empty({}); + const FieldPathPortable a({"a"}); + const FieldPathPortable b({"b"}); + const FieldPathPortable ab({"a", "b"}); + + EXPECT_TRUE(empty < a); + EXPECT_TRUE(a < b); + EXPECT_TRUE(a < ab); + + EXPECT_TRUE(a > empty); + EXPECT_TRUE(b > a); + EXPECT_TRUE(ab > a); +} + +TEST(FieldPathPortableTest, CanonicalStringOfSubstring) { + EXPECT_EQ(FieldPathPortable({"foo", "bar", "baz"}).CanonicalString(), + "foo.bar.baz"); + EXPECT_EQ(FieldPathPortable({"foo", "bar"}).CanonicalString(), "foo.bar"); + EXPECT_EQ(FieldPathPortable({"foo"}).CanonicalString(), "foo"); + EXPECT_EQ(FieldPathPortable({}).CanonicalString(), ""); +} + +TEST(FieldPath, CanonicalStringEscaping) { + // Should be escaped + EXPECT_EQ(FieldPathPortable({"1"}).CanonicalString(), "`1`"); + EXPECT_EQ(FieldPathPortable({"1ab"}).CanonicalString(), "`1ab`"); + EXPECT_EQ(FieldPathPortable({"ab!"}).CanonicalString(), "`ab!`"); + EXPECT_EQ(FieldPathPortable({"/ab"}).CanonicalString(), "`/ab`"); + EXPECT_EQ(FieldPathPortable({"a#b"}).CanonicalString(), "`a#b`"); + EXPECT_EQ(FieldPathPortable({"foo", "", "bar"}).CanonicalString(), + "foo.``.bar"); + + // Should not be escaped + EXPECT_EQ(FieldPathPortable({"_ab"}).CanonicalString(), "_ab"); + EXPECT_EQ(FieldPathPortable({"a1"}).CanonicalString(), "a1"); + EXPECT_EQ(FieldPathPortable({"a_"}).CanonicalString(), "a_"); +} + +TEST(FieldPathPortableTest, Parsing) { + EXPECT_EQ(FieldPathPortable::FromServerFormat("foo"), + FieldPathPortable({"foo"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat("foo.bar"), + FieldPathPortable({"foo", "bar"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat("foo.bar.baz"), + FieldPathPortable({"foo", "bar", "baz"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(`.foo\\`)"), + FieldPathPortable({".foo\\"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(`.foo\\`.`.foo`)"), + FieldPathPortable({".foo\\", ".foo"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(foo.`\``.bar)"), + FieldPathPortable({"foo", "`", "bar"})); + EXPECT_EQ(FieldPathPortable::FromServerFormat(R"(foo\.bar)"), + FieldPathPortable({"foo.bar"})); +} + +// This is a special case in C++: std::string may contain embedded nulls. To +// fully mimic behavior of Objective-C code, parsing must terminate upon +// encountering the first null terminator in the string. +TEST(FieldPathPortableTest, ParseEmbeddedNull) { + std::string str{"foo"}; + str += '\0'; + str += ".bar"; + + const auto path = FieldPathPortable::FromServerFormat(str); + EXPECT_EQ(path.size(), 1u); + EXPECT_EQ(path.CanonicalString(), "foo"); +} + +TEST(FieldPathPortableDeathTest, ParseFailures) { + EXPECT_DEATH(FieldPathPortable::FromServerFormat(""), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("."), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(".."), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo."), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(".bar"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo..bar"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(R"(foo\)"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat(R"(foo.\)"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo`"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("foo```"), ""); + EXPECT_DEATH(FieldPathPortable::FromServerFormat("`foo"), ""); +} + +TEST(FieldPathPortableTest, FromDotSeparatedString) { + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("a"), + FieldPathPortable({"a"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("foo"), + FieldPathPortable({"foo"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("a.b"), + FieldPathPortable({"a", "b"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("foo.bar"), + FieldPathPortable({"foo", "bar"})); + EXPECT_EQ(FieldPathPortable::FromDotSeparatedString("foo.bar.baz"), + FieldPathPortable({"foo", "bar", "baz"})); +} + +TEST(FieldPathPortableDeathTest, FromDotSeparatedStringParseFailures) { + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString(""), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString("."), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString(".foo"), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString("foo."), ""); + EXPECT_DEATH(FieldPathPortable::FromDotSeparatedString("foo..bar"), ""); +} + +TEST(FieldPathPortableTest, KeyFieldPath) { + const auto& key_field_path = FieldPathPortable::KeyFieldPath(); + EXPECT_TRUE(key_field_path.IsKeyFieldPath()); + EXPECT_EQ(key_field_path, FieldPathPortable{key_field_path}); + EXPECT_EQ(key_field_path.CanonicalString(), "__name__"); + EXPECT_EQ(key_field_path, FieldPathPortable::FromServerFormat("__name__")); + EXPECT_NE(key_field_path, FieldPathPortable::FromServerFormat( + key_field_path.CanonicalString().substr(1))); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/firebase_firestore_settings_android_test.cc b/firestore/src/tests/android/firebase_firestore_settings_android_test.cc new file mode 100644 index 0000000000..22f43f159c --- /dev/null +++ b/firestore/src/tests/android/firebase_firestore_settings_android_test.cc @@ -0,0 +1,52 @@ +#include "firestore/src/android/firebase_firestore_settings_android.h" + +#include + +#include "firestore/src/include/firebase/firestore/settings.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, ConverterBoolsAllTrue) { + JNIEnv* env = app()->GetJNIEnv(); + + Settings settings; + settings.set_host("foo"); + settings.set_ssl_enabled(true); + settings.set_persistence_enabled(true); + jobject java_settings = + FirebaseFirestoreSettingsInternal::SettingToJavaSetting(env, settings); + const Settings result = + FirebaseFirestoreSettingsInternal::JavaSettingToSetting(env, + java_settings); + EXPECT_EQ("foo", result.host()); + EXPECT_TRUE(result.is_ssl_enabled()); + EXPECT_TRUE(result.is_persistence_enabled()); + + env->DeleteLocalRef(java_settings); +} + +TEST_F(FirestoreIntegrationTest, ConverterBoolsAllFalse) { + JNIEnv* env = app()->GetJNIEnv(); + + Settings settings; + settings.set_host("bar"); + settings.set_ssl_enabled(false); + settings.set_persistence_enabled(false); + jobject java_settings = + FirebaseFirestoreSettingsInternal::SettingToJavaSetting(env, settings); + const Settings result = + FirebaseFirestoreSettingsInternal::JavaSettingToSetting(env, + java_settings); + EXPECT_EQ("bar", result.host()); + EXPECT_FALSE(result.is_ssl_enabled()); + EXPECT_FALSE(result.is_persistence_enabled()); + + env->DeleteLocalRef(java_settings); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/geo_point_android_test.cc b/firestore/src/tests/android/geo_point_android_test.cc new file mode 100644 index 0000000000..8d09aee483 --- /dev/null +++ b/firestore/src/tests/android/geo_point_android_test.cc @@ -0,0 +1,24 @@ +#include "firestore/src/android/geo_point_android.h" + +#include + +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/geo_point.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, Converter) { + JNIEnv* env = app()->GetJNIEnv(); + + const GeoPoint point{12.0, 34.0}; + jobject java_point = GeoPointInternal::GeoPointToJavaGeoPoint(env, point); + EXPECT_EQ(point, GeoPointInternal::JavaGeoPointToGeoPoint(env, java_point)); + + env->DeleteLocalRef(java_point); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/snapshot_metadata_android_test.cc b/firestore/src/tests/android/snapshot_metadata_android_test.cc new file mode 100644 index 0000000000..3781697f78 --- /dev/null +++ b/firestore/src/tests/android/snapshot_metadata_android_test.cc @@ -0,0 +1,42 @@ +#include "firestore/src/android/snapshot_metadata_android.h" + +#include + +#include "firestore/src/include/firebase/firestore/snapshot_metadata.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, ConvertHasPendingWrites) { + JNIEnv* env = app()->GetJNIEnv(); + + const SnapshotMetadata has_pending_writes{/*has_pending_writes=*/true, + /*is_from_cache=*/false}; + // The converter will delete local reference for us. + const SnapshotMetadata result = + SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata( + env, SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata( + env, has_pending_writes)); + EXPECT_TRUE(result.has_pending_writes()); + EXPECT_FALSE(result.is_from_cache()); +} + +TEST_F(FirestoreIntegrationTest, ConvertIsFromCache) { + JNIEnv* env = app()->GetJNIEnv(); + + const SnapshotMetadata is_from_cache{/*has_pending_writes=*/false, + /*is_from_cache=*/true}; + // The converter will delete local reference for us. + const SnapshotMetadata result = + SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata( + env, SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata( + env, is_from_cache)); + EXPECT_FALSE(result.has_pending_writes()); + EXPECT_TRUE(result.is_from_cache()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/android/timestamp_android_test.cc b/firestore/src/tests/android/timestamp_android_test.cc new file mode 100644 index 0000000000..a30c5373c9 --- /dev/null +++ b/firestore/src/tests/android/timestamp_android_test.cc @@ -0,0 +1,26 @@ +#include "firestore/src/android/timestamp_android.h" + +#include + +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/timestamp.h" + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, Converter) { + JNIEnv* env = app()->GetJNIEnv(); + + const Timestamp timestamp{1234, 5678}; + jobject java_timestamp = + TimestampInternal::TimestampToJavaTimestamp(env, timestamp); + EXPECT_EQ(timestamp, + TimestampInternal::JavaTimestampToTimestamp(env, java_timestamp)); + + env->DeleteLocalRef(java_timestamp); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/array_transform_test.cc b/firestore/src/tests/array_transform_test.cc new file mode 100644 index 0000000000..38ce96cb35 --- /dev/null +++ b/firestore/src/tests/array_transform_test.cc @@ -0,0 +1,230 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRArrayTransformTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ArrayTransformsTest.java +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ArrayTransformServerApplicationTest.java + +namespace firebase { +namespace firestore { + +class ArrayTransformTest : public FirestoreIntegrationTest { + protected: + void SetUp() override { + document_ = Document(); + registration_ = accumulator_.listener()->AttachTo( + &document_, MetadataChanges::kInclude); + + // Wait for initial null snapshot to avoid potential races. + DocumentSnapshot snapshot = accumulator_.Await(); + EXPECT_FALSE(snapshot.exists()); + } + + void TearDown() override { registration_.Remove(); } + + void WriteInitialData(const MapFieldValue& data) { + Await(document_.Set(data)); + ExpectLocalAndRemoteEvent(data); + } + + void ExpectLocalAndRemoteEvent(const MapFieldValue& data) { + EXPECT_THAT(accumulator_.AwaitLocalEvent().GetData(), + testing::ContainerEq(data)); + EXPECT_THAT(accumulator_.AwaitRemoteEvent().GetData(), + testing::ContainerEq(data)); + } + + DocumentReference document_; + EventAccumulator accumulator_; + ListenerRegistration registration_; +}; + +class ArrayTransformServerApplicationTest : public FirestoreIntegrationTest { + protected: + void SetUp() override { document_ = Document(); } + + DocumentReference document_; +}; + +TEST_F(ArrayTransformTest, CreateDocumentWithArrayUnion) { + Await(document_.Set(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(2)})}}); +} + +TEST_F(ArrayTransformTest, AppendToArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Update(MapFieldValue{ + {"array", + FieldValue::ArrayUnion({FieldValue::Integer(2), FieldValue::Integer(1), + FieldValue::Integer(4)})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(2), FieldValue::Integer(4)})}}); +} + +TEST_F(ArrayTransformTest, AppendToArrayViaMergeSet) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Set(MapFieldValue{{"array", FieldValue::ArrayUnion( + {FieldValue::Integer(2), + FieldValue::Integer(1), + FieldValue::Integer(4)})}}, + SetOptions::Merge())); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(2), FieldValue::Integer(4)})}}); +} + +TEST_F(ArrayTransformTest, AppendObjectToArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::String("hi")}})})}}); + Await(document_.Update(MapFieldValue{ + {"array", + FieldValue::ArrayUnion( + {{FieldValue::Map({{"a", FieldValue::String("hi")}})}, + {FieldValue::Map({{"a", FieldValue::String("bye")}})}})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", FieldValue::Array( + {{FieldValue::Map({{"a", FieldValue::String("hi")}})}, + {FieldValue::Map({{"a", FieldValue::String("bye")}})}})}}); +} + +TEST_F(ArrayTransformTest, RemoveFromArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayRemove( + {FieldValue::Integer(1), FieldValue::Integer(4)})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(3), FieldValue::Integer(3)})}}); +} + +TEST_F(ArrayTransformTest, RemoveFromArrayViaMergeSet) { + WriteInitialData(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(1), FieldValue::Integer(3), + FieldValue::Integer(1), FieldValue::Integer(3)})}}); + Await(document_.Set(MapFieldValue{{"array", FieldValue::ArrayRemove( + {FieldValue::Integer(1), + FieldValue::Integer(4)})}}, + SetOptions::Merge())); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(3), FieldValue::Integer(3)})}}); +} + +TEST_F(ArrayTransformTest, RemoveObjectFromArrayViaUpdate) { + WriteInitialData(MapFieldValue{ + {"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::String("hi")}}), + FieldValue::Map({{"a", FieldValue::String("bye")}})})}}); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayRemove( + {{FieldValue::Map({{"a", FieldValue::String("hi")}})}})}})); + ExpectLocalAndRemoteEvent(MapFieldValue{ + {"array", FieldValue::Array( + {{FieldValue::Map({{"a", FieldValue::String("bye")}})}})}}); +} + +TEST_F(ArrayTransformServerApplicationTest, SetWithNoCachedBaseDoc) { + Await(document_.Set(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(1), + FieldValue::Integer(2)})}})); +} + +TEST_F(ArrayTransformServerApplicationTest, UpdateWithNoCachedBaseDoc) { + // Write an initial document in an isolated Firestore instance so it's not + // stored in our cache. + Await(CachedFirestore("isolated") + ->Document(document_.path()) + .Set(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42)})}})); + + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + + // Nothing should be cached since it was an update and we had no base doc. + Future future = document_.Get(Source::kCache); + Await(future); + EXPECT_EQ(Error::kErrorUnavailable, future.error()); +} + +TEST_F(ArrayTransformServerApplicationTest, MergeSetWithNoCachedBaseDoc) { + // Write an initial document in an isolated Firestore instance so it's not + // stored in our cache. + Await(CachedFirestore("isolated") + ->Document(document_.path()) + .Set(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42)})}})); + + Await(document_.Set(MapFieldValue{{"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), + FieldValue::Integer(2)})}}, + SetOptions::Merge())); + // Document will be cached but we'll be missing 42. + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(1), + FieldValue::Integer(2)})}})); +} + +TEST_F(ArrayTransformServerApplicationTest, + UpdateWithCachedBaseDocUsingArrayUnion) { + Await(document_.Set( + MapFieldValue{{"array", FieldValue::Array({FieldValue::Integer(42)})}})); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayUnion( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42), + FieldValue::Integer(1), + FieldValue::Integer(2)})}})); +} + +TEST_F(ArrayTransformServerApplicationTest, + UpdateWithCachedBaseDocUsingArrayRemove) { + Await(document_.Set(MapFieldValue{ + {"array", + FieldValue::Array({FieldValue::Integer(42), FieldValue::Integer(1L), + FieldValue::Integer(2L)})}})); + Await(document_.Update(MapFieldValue{ + {"array", FieldValue::ArrayRemove( + {FieldValue::Integer(1), FieldValue::Integer(2)})}})); + DocumentSnapshot snapshot = *Await(document_.Get(Source::kCache)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"array", FieldValue::Array({FieldValue::Integer(42)})}})); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/cleanup_test.cc b/firestore/src/tests/cleanup_test.cc new file mode 100644 index 0000000000..f23ee29be7 --- /dev/null +++ b/firestore/src/tests/cleanup_test.cc @@ -0,0 +1,420 @@ +#include "app/src/include/firebase/internal/common.h" +#include "firestore/src/common/futures.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" + +namespace firebase { +namespace firestore { +namespace { + +void ExpectAllMethodsAreNoOps(Query* ptr); + +// Checks that methods accessing the associated Firestore instance don't crash +// and return null. +template +void ExpectNullFirestore(T* ptr) { + EXPECT_EQ(ptr->firestore(), nullptr); + // Make sure to check both const and non-const overloads. + EXPECT_EQ(static_cast(ptr)->firestore(), nullptr); +} + +// Checks that the given object can be copied from, and the resulting copy can +// be moved. +template +void ExpectCopyableAndMoveable(T* ptr) { + EXPECT_NO_THROW({ + // Copy constructor + T copy = *ptr; + // Move constructor + T moved = std::move(copy); + + // Copy assignment operator + copy = *ptr; + // Move assignment operator + moved = std::move(copy); + }); +} + +// Checks that `operator==` and `operator!=` work correctly by comparing to +// a default-constructed instance. +template +void ExpectEqualityToWork(T* ptr) { + EXPECT_TRUE(*ptr == T()); + EXPECT_FALSE(*ptr != T()); +} + +// `ExpectAllMethodsAreNoOps` calls all the public API methods on the given +// `ptr` and checks that the calls don't crash and, where applicable, return +// value-initialized values. + +void ExpectAllMethodsAreNoOps(CollectionReference* ptr) { + EXPECT_EQ(*ptr, CollectionReference()); + ExpectCopyableAndMoveable(ptr); + ExpectEqualityToWork(ptr); + + ExpectAllMethodsAreNoOps(static_cast(ptr)); + + EXPECT_EQ(ptr->id(), ""); + EXPECT_EQ(ptr->path(), ""); + + EXPECT_EQ(ptr->Document(), DocumentReference()); + EXPECT_EQ(ptr->Document("foo"), DocumentReference()); + EXPECT_EQ(ptr->Document(std::string("foo")), DocumentReference()); + + EXPECT_EQ(ptr->Add(MapFieldValue()), FailedFuture()); +} + +void ExpectAllMethodsAreNoOps(DocumentChange* ptr) { + // TODO(b/137966104): implement == on `DocumentChange` + // ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_EQ(ptr->type(), DocumentChange::Type()); + // TODO(b/137966104): implement == on `DocumentSnapshot` + EXPECT_NO_THROW(ptr->document()); + EXPECT_EQ(ptr->old_index(), 0); + EXPECT_EQ(ptr->new_index(), 0); +} + +void ExpectAllMethodsAreNoOps(DocumentReference* ptr) { + EXPECT_FALSE(ptr->is_valid()); + + ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + ExpectNullFirestore(ptr); + + EXPECT_EQ(ptr->ToString(), "DocumentReference(invalid)"); + + EXPECT_EQ(ptr->id(), ""); + EXPECT_EQ(ptr->path(), ""); + + EXPECT_EQ(ptr->Parent(), CollectionReference()); + EXPECT_EQ(ptr->Collection("foo"), CollectionReference()); + EXPECT_EQ(ptr->Collection(std::string("foo")), CollectionReference()); + + EXPECT_EQ(ptr->Get(), FailedFuture()); + + EXPECT_EQ(ptr->Set(MapFieldValue()), FailedFuture()); + + EXPECT_EQ(ptr->Update(MapFieldValue()), FailedFuture()); + EXPECT_EQ(ptr->Update(MapFieldPathValue()), FailedFuture()); + + EXPECT_EQ(ptr->Delete(), FailedFuture()); + +#if defined(FIREBASE_USE_STD_FUNCTION) + EXPECT_NO_THROW( + ptr->AddSnapshotListener([](const DocumentSnapshot&, Error) {})); +#else + EXPECT_NO_THROW(ptr->AddSnapshotListener(nullptr)); +#endif +} + +void ExpectAllMethodsAreNoOps(DocumentSnapshot* ptr) { + // TODO(b/137966104): implement == on `DocumentSnapshot` + // ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_EQ(ptr->ToString(), "DocumentSnapshot(invalid)"); + + EXPECT_EQ(ptr->id(), ""); + EXPECT_FALSE(ptr->exists()); + + EXPECT_EQ(ptr->reference(), DocumentReference()); + // TODO(b/137966104): implement == on `SnapshotMetadata` + EXPECT_NO_THROW(ptr->metadata()); + + EXPECT_EQ(ptr->GetData(), MapFieldValue()); + + EXPECT_EQ(ptr->Get("foo"), FieldValue()); + EXPECT_EQ(ptr->Get(std::string("foo")), FieldValue()); + EXPECT_EQ(ptr->Get(FieldPath{"foo"}), FieldValue()); +} + +void ExpectAllMethodsAreNoOps(FieldValue* ptr) { + ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_FALSE(ptr->is_valid()); + // FieldValue doesn't have a separate "invalid" type in its enum. + EXPECT_TRUE(ptr->is_null()); + + EXPECT_EQ(ptr->type(), FieldValue::Type()); + + EXPECT_FALSE(ptr->is_boolean()); + EXPECT_FALSE(ptr->is_integer()); + EXPECT_FALSE(ptr->is_double()); + EXPECT_FALSE(ptr->is_timestamp()); + EXPECT_FALSE(ptr->is_string()); + EXPECT_FALSE(ptr->is_blob()); + EXPECT_FALSE(ptr->is_reference()); + EXPECT_FALSE(ptr->is_geo_point()); + EXPECT_FALSE(ptr->is_array()); + EXPECT_FALSE(ptr->is_map()); + + EXPECT_EQ(ptr->boolean_value(), false); + EXPECT_EQ(ptr->integer_value(), 0); + EXPECT_EQ(ptr->double_value(), 0); + EXPECT_EQ(ptr->timestamp_value(), Timestamp()); + EXPECT_EQ(ptr->string_value(), ""); + EXPECT_EQ(ptr->blob_value(), nullptr); + EXPECT_EQ(ptr->reference_value(), DocumentReference()); + EXPECT_EQ(ptr->geo_point_value(), GeoPoint()); + EXPECT_TRUE(ptr->array_value().empty()); + EXPECT_TRUE(ptr->map_value().empty()); +} + +void ExpectAllMethodsAreNoOps(ListenerRegistration* ptr) { + // `ListenerRegistration` isn't equality comparable. + ExpectCopyableAndMoveable(ptr); + + EXPECT_NO_THROW(ptr->Remove()); +} + +void ExpectAllMethodsAreNoOps(Query* ptr) { + ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + ExpectNullFirestore(ptr); + + EXPECT_EQ(ptr->WhereEqualTo("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereEqualTo(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->WhereLessThan("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereLessThan(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->WhereLessThanOrEqualTo("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereLessThanOrEqualTo(FieldPath{"foo"}, FieldValue()), + Query()); + + EXPECT_EQ(ptr->WhereGreaterThan("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereGreaterThan(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->WhereGreaterThanOrEqualTo("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereGreaterThanOrEqualTo(FieldPath{"foo"}, FieldValue()), + Query()); + + EXPECT_EQ(ptr->WhereArrayContains("foo", FieldValue()), Query()); + EXPECT_EQ(ptr->WhereArrayContains(FieldPath{"foo"}, FieldValue()), Query()); + + EXPECT_EQ(ptr->OrderBy("foo"), Query()); + EXPECT_EQ(ptr->OrderBy(FieldPath{"foo"}), Query()); + + EXPECT_EQ(ptr->Limit(123), Query()); + + EXPECT_EQ(ptr->StartAt(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->StartAt(std::vector()), Query()); + + EXPECT_EQ(ptr->StartAfter(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->StartAfter(std::vector()), Query()); + + EXPECT_EQ(ptr->EndBefore(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->EndBefore(std::vector()), Query()); + + EXPECT_EQ(ptr->EndAt(DocumentSnapshot()), Query()); + EXPECT_EQ(ptr->EndAt(std::vector()), Query()); + + EXPECT_EQ(ptr->Get(), FailedFuture()); + + EXPECT_EQ(ptr->Get(), FailedFuture()); + +#if defined(FIREBASE_USE_STD_FUNCTION) + EXPECT_NO_THROW(ptr->AddSnapshotListener([](const QuerySnapshot&, Error) {})); +#else + EXPECT_NO_THROW(ptr->AddSnapshotListener(nullptr)); +#endif +} + +void ExpectAllMethodsAreNoOps(QuerySnapshot* ptr) { + // TODO(b/137966104): implement == on `QuerySnapshot` + // ExpectEqualityToWork(ptr); + ExpectCopyableAndMoveable(ptr); + + EXPECT_EQ(ptr->query(), Query()); + + // TODO(b/137966104): implement == on `SnapshotMetadata` + EXPECT_NO_THROW(ptr->metadata()); + + EXPECT_TRUE(ptr->DocumentChanges().empty()); + EXPECT_TRUE(ptr->documents().empty()); + EXPECT_TRUE(ptr->empty()); + EXPECT_EQ(ptr->size(), 0); +} + +void ExpectAllMethodsAreNoOps(WriteBatch* ptr) { + // `WriteBatch` isn't equality comparable. + ExpectCopyableAndMoveable(ptr); + + EXPECT_NO_THROW(ptr->Set(DocumentReference(), MapFieldValue())); + + EXPECT_NO_THROW(ptr->Update(DocumentReference(), MapFieldValue())); + EXPECT_NO_THROW(ptr->Update(DocumentReference(), MapFieldPathValue())); + + EXPECT_NO_THROW(ptr->Delete(DocumentReference())); + + EXPECT_EQ(ptr->Commit(), FailedFuture()); +} + +using CleanupTest = FirestoreIntegrationTest; + +TEST_F(CleanupTest, CollectionReferenceIsBlankAfterCleanup) { + { + CollectionReference default_constructed; + SCOPED_TRACE("CollectionReference.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + CollectionReference col = Collection(); + DeleteFirestore(); + SCOPED_TRACE("CollectionReference.AfterCleanup"); + ExpectAllMethodsAreNoOps(&col); +} + +TEST_F(CleanupTest, DocumentChangeIsBlankAfterCleanup) { + { + DocumentChange default_constructed; + SCOPED_TRACE("DocumentChange.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + CollectionReference col = Collection("col"); + DocumentReference doc = col.Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + + QuerySnapshot snap = ReadDocuments(col); + auto changes = snap.DocumentChanges(); + ASSERT_EQ(changes.size(), 1); + DocumentChange& change = changes.front(); + + DeleteFirestore(); + SCOPED_TRACE("DocumentChange.AfterCleanup"); + ExpectAllMethodsAreNoOps(&change); +} + +TEST_F(CleanupTest, DocumentReferenceIsBlankAfterCleanup) { + { + DocumentReference default_constructed; + SCOPED_TRACE("DocumentReference.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + DeleteFirestore(); + SCOPED_TRACE("DocumentReference.AfterCleanup"); + ExpectAllMethodsAreNoOps(&doc); +} + +TEST_F(CleanupTest, DocumentSnapshotIsBlankAfterCleanup) { + { + DocumentSnapshot default_constructed; + SCOPED_TRACE("DocumentSnapshot.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + DocumentSnapshot snap = ReadDocument(doc); + + DeleteFirestore(); + SCOPED_TRACE("DocumentSnapshot.AfterCleanup"); + ExpectAllMethodsAreNoOps(&snap); +} + +TEST_F(CleanupTest, FieldValueIsBlankAfterCleanup) { + { + FieldValue default_constructed; + SCOPED_TRACE("FieldValue.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}, + {"ref", FieldValue::Reference(doc)}}); + DocumentSnapshot snap = ReadDocument(doc); + + FieldValue str_value = snap.Get("foo"); + EXPECT_TRUE(str_value.is_valid()); + EXPECT_TRUE(str_value.is_string()); + + FieldValue ref_value = snap.Get("ref"); + EXPECT_TRUE(ref_value.is_valid()); + EXPECT_TRUE(ref_value.is_reference()); + + DeleteFirestore(); + // `FieldValue`s are not cleaned up, because they are owned by the user and + // stay valid after Firestore has shut down. + EXPECT_TRUE(str_value.is_valid()); + EXPECT_TRUE(str_value.is_string()); + EXPECT_EQ(str_value.string_value(), "bar"); + + // However, need to make sure that in a reference value, the reference was + // cleaned up. + EXPECT_TRUE(ref_value.is_valid()); + EXPECT_TRUE(ref_value.is_reference()); + DocumentReference ref_after_cleanup = ref_value.reference_value(); + SCOPED_TRACE("FieldValue.AfterCleanup"); + ExpectAllMethodsAreNoOps(&ref_after_cleanup); +} + +// Note: `Firestore` is not default-constructible, and it is deleted immediately +// after cleanup. Thus, there is no case where a user could be accessing +// a "blank" Firestore instance. + +#if defined(FIREBASE_USE_STD_FUNCTION) +TEST_F(CleanupTest, ListenerRegistrationIsBlankAfterCleanup) { + { + ListenerRegistration default_constructed; + SCOPED_TRACE("ListenerRegistration.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + DocumentReference doc = Document(); + ListenerRegistration reg = + doc.AddSnapshotListener([](const DocumentSnapshot&, Error) {}); + DeleteFirestore(); + SCOPED_TRACE("ListenerRegistration.AfterCleanup"); + ExpectAllMethodsAreNoOps(®); +} +#endif + +// Note: `Query` cleanup is tested as part of `CollectionReference` cleanup +// (`CollectionReference` is derived from `Query`). + +TEST_F(CleanupTest, QuerySnapshotIsBlankAfterCleanup) { + { + QuerySnapshot default_constructed; + SCOPED_TRACE("QuerySnapshot.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + CollectionReference col = Collection("col"); + DocumentReference doc = col.Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + + QuerySnapshot snap = ReadDocuments(col); + EXPECT_EQ(snap.size(), 1); + + DeleteFirestore(); + SCOPED_TRACE("QuerySnapshot.AfterCleanup"); + ExpectAllMethodsAreNoOps(&snap); +} + +// Note: `Transaction` is uncopyable and not default constructible, and storing +// a pointer to a `Transaction` is not valid in general, because the object will +// be destroyed as soon as the transaction is finished. Thus, there is no valid +// case where a user could be accessing a "blank" transaction. + +TEST_F(CleanupTest, WriteBatchIsBlankAfterCleanup) { + { + WriteBatch default_constructed; + SCOPED_TRACE("WriteBatch.DefaultConstructed"); + ExpectAllMethodsAreNoOps(&default_constructed); + } + + WriteBatch batch = firestore()->batch(); + DeleteFirestore(); + SCOPED_TRACE("WriteBatch.AfterCleanup"); + ExpectAllMethodsAreNoOps(&batch); +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/collection_reference_test.cc b/firestore/src/tests/collection_reference_test.cc new file mode 100644 index 0000000000..1c700497b9 --- /dev/null +++ b/firestore/src/tests/collection_reference_test.cc @@ -0,0 +1,34 @@ +#include + +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/collection_reference_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/collection_reference_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using CollectionReferenceTest = testing::Test; + +TEST_F(CollectionReferenceTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(CollectionReferenceTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/cursor_test.cc b/firestore/src/tests/cursor_test.cc new file mode 100644 index 0000000000..de9e6857fe --- /dev/null +++ b/firestore/src/tests/cursor_test.cc @@ -0,0 +1,278 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRCursorTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/CursorTest.java +// The iOS test names start with the mandatory test prefix while Android test +// names do not. Here we use the Android test names. + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, CanPageThroughItems) { + CollectionReference collection = + Collection({{"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}, + {"c", {{"v", FieldValue::String("c")}}}, + {"d", {{"v", FieldValue::String("d")}}}, + {"e", {{"v", FieldValue::String("e")}}}, + {"f", {{"v", FieldValue::String("f")}}}}); + QuerySnapshot snapshot = ReadDocuments(collection.Limit(2)); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(snapshot)); + + DocumentSnapshot last_doc = snapshot.documents()[1]; + snapshot = ReadDocuments(collection.Limit(3).StartAfter(last_doc)); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("c")}}, + {{"v", FieldValue::String("d")}}, + {{"v", FieldValue::String("e")}}}), + QuerySnapshotToValues(snapshot)); + + last_doc = snapshot.documents()[2]; + snapshot = ReadDocuments(collection.Limit(1).StartAfter(last_doc)); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("f")}}}), + QuerySnapshotToValues(snapshot)); + + last_doc = snapshot.documents()[0]; + snapshot = ReadDocuments(collection.Limit(3).StartAfter(last_doc)); + EXPECT_EQ(std::vector{}, QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, CanBeCreatedFromDocuments) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Double(2.0)}}}, + {"e", + {{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}}, + // should not show up + {"f", + {{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + Query query = collection.OrderBy("sort"); + DocumentSnapshot snapshot = ReadDocument(collection.Document("c")); + + EXPECT_TRUE(snapshot.exists()); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("c")}, + {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("d")}, + {"sort", FieldValue::Double(2.0)}}}), + QuerySnapshotToValues(ReadDocuments(query.StartAt(snapshot)))); + + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}, + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}, + {{"k", FieldValue::String("b")}, + {"sort", FieldValue::Double(2.0)}}}), + QuerySnapshotToValues(ReadDocuments(query.EndBefore(snapshot)))); +} + +TEST_F(FirestoreIntegrationTest, CanBeCreatedFromValues) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Double(2.0)}}}, + {"e", + {{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}}, + // should not show up + {"f", + {{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + Query query = collection.OrderBy("sort"); + + QuerySnapshot snapshot = ReadDocuments( + query.StartAt(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("d")}, + {"sort", FieldValue::Double(2.0)}}}), + QuerySnapshotToValues(snapshot)); + + snapshot = ReadDocuments( + query.EndBefore(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Double(0.0)}}, + {{"k", FieldValue::String("a")}, + {"sort", FieldValue::Double(1.0)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, CanBeCreatedUsingDocumentId) { + std::map docs = { + {"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}, + {"c", {{"v", FieldValue::String("c")}}}, + {"d", {{"v", FieldValue::String("d")}}}, + {"e", {{"v", FieldValue::String("e")}}}}; + + CollectionReference writer = CachedFirestore("writer") + ->Collection("parent-collection") + .Document() + .Collection("sub-collection"); + WriteDocuments(writer, docs); + + CollectionReference reader = + CachedFirestore("reader")->Collection(writer.path()); + QuerySnapshot snapshot = ReadDocuments( + reader.OrderBy(FieldPath::DocumentId()) + .StartAt(std::vector({FieldValue::String("b")})) + .EndBefore(std::vector({FieldValue::String("d")}))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("b")}}, + {{"v", FieldValue::String("c")}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, CanBeUsedWithReferenceValues) { + Firestore* db = firestore(); + std::map docs = { + {"a", + {{"k", FieldValue::String("1a")}, + {"ref", FieldValue::Reference(db->Collection("1").Document("a"))}}}, + {"b", + {{"k", FieldValue::String("1b")}, + {"ref", FieldValue::Reference(db->Collection("1").Document("b"))}}}, + {"c", + {{"k", FieldValue::String("2a")}, + {"ref", FieldValue::Reference(db->Collection("2").Document("a"))}}}, + {"d", + {{"k", FieldValue::String("2b")}, + {"ref", FieldValue::Reference(db->Collection("2").Document("b"))}}}, + {"e", + {{"k", FieldValue::String("3a")}, + {"ref", FieldValue::Reference(db->Collection("3").Document("a"))}}}}; + + CollectionReference collection = Collection(docs); + + QuerySnapshot snapshot = ReadDocuments( + collection.OrderBy("ref") + .StartAfter(std::vector( + {FieldValue::Reference(db->Collection("1").Document("a"))})) + .EndAt(std::vector( + {FieldValue::Reference(db->Collection("2").Document("b"))}))); + + std::vector results; + for (const DocumentSnapshot& doc : snapshot.documents()) { + results.push_back(doc.Get("k").string_value()); + } + EXPECT_EQ(std::vector({"1b", "2a", "2b"}), results); +} + +TEST_F(FirestoreIntegrationTest, CanBeUsedInDescendingQueries) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Double(3.0)}}}, + {"e", + {{"k", FieldValue::String("e")}, {"sort", FieldValue::Double(0.0)}}}, + // should not show up + {"f", + {{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + Query query = + collection.OrderBy("sort", Query::Direction::kDescending) + .OrderBy(FieldPath::DocumentId(), Query::Direction::kDescending); + + QuerySnapshot snapshot = ReadDocuments( + query.StartAt(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("c")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Double(2.0)}}, + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Double(1.0)}}, + {{"k", FieldValue::String("e")}, + {"sort", FieldValue::Double(0.0)}}}), + QuerySnapshotToValues(snapshot)); + + snapshot = ReadDocuments( + query.EndBefore(std::vector({FieldValue::Double(2.0)}))); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("d")}, + {"sort", FieldValue::Double(3.0)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TimestampsCanBePassedToQueriesAsLimits) { + CollectionReference collection = + Collection({{"a", {{"timestamp", FieldValue::Timestamp({100, 2000})}}}, + {"b", {{"timestamp", FieldValue::Timestamp({100, 5000})}}}, + {"c", {{"timestamp", FieldValue::Timestamp({100, 3000})}}}, + {"d", {{"timestamp", FieldValue::Timestamp({100, 1000})}}}, + // Number of nanoseconds deliberately repeated. + {"e", {{"timestamp", FieldValue::Timestamp({100, 5000})}}}, + {"f", {{"timestamp", FieldValue::Timestamp({100, 4000})}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.OrderBy("timestamp") + .StartAfter( + std::vector({FieldValue::Timestamp({100, 2000})})) + .EndAt( + std::vector({FieldValue::Timestamp({100, 5000})}))); + EXPECT_EQ(std::vector({"c", "f", "b", "e"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TimestampsCanBePassedToQueriesInWhereClause) { + CollectionReference collection = + Collection({{"a", {{"timestamp", FieldValue::Timestamp({100, 7000})}}}, + {"b", {{"timestamp", FieldValue::Timestamp({100, 4000})}}}, + {"c", {{"timestamp", FieldValue::Timestamp({100, 8000})}}}, + {"d", {{"timestamp", FieldValue::Timestamp({100, 5000})}}}, + {"e", {{"timestamp", FieldValue::Timestamp({100, 6000})}}}}); + + QuerySnapshot snapshot = ReadDocuments( + collection + .WhereGreaterThanOrEqualTo("timestamp", + FieldValue::Timestamp({100, 5000})) + .WhereLessThan("timestamp", FieldValue::Timestamp({100, 8000}))); + EXPECT_EQ(std::vector({"d", "e", "a"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TimestampsAreTruncatedToMicroseconds) { + const FieldValue nanos = FieldValue::Timestamp({0, 123456789}); + const FieldValue micros = FieldValue::Timestamp({0, 123456000}); + const FieldValue millis = FieldValue::Timestamp({0, 123000000}); + CollectionReference collection = Collection({{"a", {{"timestamp", nanos}}}}); + + QuerySnapshot snapshot = + ReadDocuments(collection.WhereEqualTo("timestamp", nanos)); + EXPECT_EQ(1, QuerySnapshotToValues(snapshot).size()); + + // Because Timestamp should have been truncated to microseconds, the + // microsecond timestamp should be considered equal to the nanosecond one. + snapshot = ReadDocuments(collection.WhereEqualTo("timestamp", micros)); + EXPECT_EQ(1, QuerySnapshotToValues(snapshot).size()); + + // The truncation is just to the microseconds, however, so the millisecond + // timestamp should be treated as different and thus the query should return + // no results. + snapshot = ReadDocuments(collection.WhereEqualTo("timestamp", millis)); + EXPECT_TRUE(QuerySnapshotToValues(snapshot).empty()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/document_change_test.cc b/firestore/src/tests/document_change_test.cc new file mode 100644 index 0000000000..20d4f902e0 --- /dev/null +++ b/firestore/src/tests/document_change_test.cc @@ -0,0 +1,31 @@ +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#if defined(__ANDROID__) +#include "firestore/src/android/document_change_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/document_change_stub.h" +#endif // defined(__ANDROID__) + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using DocumentChangeTest = testing::Test; + +TEST_F(DocumentChangeTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(DocumentChangeTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/document_reference_test.cc b/firestore/src/tests/document_reference_test.cc new file mode 100644 index 0000000000..64e66f634c --- /dev/null +++ b/firestore/src/tests/document_reference_test.cc @@ -0,0 +1,34 @@ +#include + +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/document_reference_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/document_reference_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using DocumentReferenceTest = testing::Test; + +TEST_F(DocumentReferenceTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(DocumentReferenceTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/document_snapshot_test.cc b/firestore/src/tests/document_snapshot_test.cc new file mode 100644 index 0000000000..5c995b734b --- /dev/null +++ b/firestore/src/tests/document_snapshot_test.cc @@ -0,0 +1,35 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/document_snapshot_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/stub/document_snapshot_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using DocumentSnapshotTest = testing::Test; + +TEST_F(DocumentSnapshotTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(DocumentSnapshotTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/field_value_test.cc b/firestore/src/tests/field_value_test.cc new file mode 100644 index 0000000000..aaf382ea92 --- /dev/null +++ b/firestore/src/tests/field_value_test.cc @@ -0,0 +1,331 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#if defined(__ANDROID__) +#include "firestore/src/android/field_value_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/field_value_stub.h" +#else +#include "firestore/src/ios/field_value_ios.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +using Type = FieldValue::Type; +using FieldValueTest = testing::Test; + +// Sanity test for stubs +TEST_F(FirestoreIntegrationTest, TestFieldValueTypes) { + ASSERT_NO_THROW({ + FieldValue::Null(); + FieldValue::Boolean(true); + FieldValue::Integer(123L); + FieldValue::Double(3.1415926); + FieldValue::Timestamp({12345, 54321}); + FieldValue::String("hello"); + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + FieldValue::Blob(blob, sizeof(blob)); + FieldValue::GeoPoint({43, 80}); + FieldValue::Array(std::vector{FieldValue::Null()}); + FieldValue::Map(MapFieldValue{{"Null", FieldValue::Null()}}); + FieldValue::Delete(); + FieldValue::ServerTimestamp(); + FieldValue::ArrayUnion(std::vector{FieldValue::Null()}); + FieldValue::ArrayRemove(std::vector{FieldValue::Null()}); + }); +} + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +TEST_F(FieldValueTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(FieldValueTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +#if !defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestNullType) { + FieldValue value = FieldValue::Null(); + EXPECT_EQ(Type::kNull, value.type()); +} + +TEST_F(FirestoreIntegrationTest, TestBooleanType) { + FieldValue value = FieldValue::Boolean(true); + EXPECT_EQ(Type::kBoolean, value.type()); + EXPECT_EQ(true, value.boolean_value()); +} + +TEST_F(FirestoreIntegrationTest, TestIntegerType) { + FieldValue value = FieldValue::Integer(123); + EXPECT_EQ(Type::kInteger, value.type()); + EXPECT_EQ(123, value.integer_value()); +} + +TEST_F(FirestoreIntegrationTest, TestDoubleType) { + FieldValue value = FieldValue::Double(3.1415926); + EXPECT_EQ(Type::kDouble, value.type()); + EXPECT_EQ(3.1415926, value.double_value()); +} + +TEST_F(FirestoreIntegrationTest, TestTimestampType) { + FieldValue value = FieldValue::Timestamp({12345, 54321}); + EXPECT_EQ(Type::kTimestamp, value.type()); + EXPECT_EQ(Timestamp(12345, 54321), value.timestamp_value()); +} + +TEST_F(FirestoreIntegrationTest, TestStringType) { + FieldValue value = FieldValue::String("hello"); + EXPECT_EQ(Type::kString, value.type()); + EXPECT_STREQ("hello", value.string_value().c_str()); +} + +TEST_F(FirestoreIntegrationTest, TestBlobType) { + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + FieldValue value = FieldValue::Blob(blob, sizeof(blob)); + EXPECT_EQ(Type::kBlob, value.type()); + EXPECT_EQ(sizeof(blob), value.blob_size()); + const uint8_t* value_blob = value.blob_value(); + + FieldValue copied(value); + EXPECT_EQ(Type::kBlob, copied.type()); + EXPECT_EQ(sizeof(blob), copied.blob_size()); + const uint8_t* copied_blob = copied.blob_value(); + + for (int i = 0; i < sizeof(blob); ++i) { + EXPECT_EQ(blob[i], value_blob[i]); + EXPECT_EQ(blob[i], copied_blob[i]); + } +} + +TEST_F(FirestoreIntegrationTest, TestReferenceType) { + FieldValue value = FieldValue::Reference(firestore()->Document("foo/bar")); + EXPECT_EQ(Type::kReference, value.type()); + EXPECT_EQ(value.reference_value().path(), "foo/bar"); +} + +TEST_F(FirestoreIntegrationTest, TestGeoPointType) { + FieldValue value = FieldValue::GeoPoint({43, 80}); + EXPECT_EQ(Type::kGeoPoint, value.type()); + EXPECT_EQ(GeoPoint(43, 80), value.geo_point_value()); +} + +TEST_F(FirestoreIntegrationTest, TestArrayType) { + FieldValue value = FieldValue::Array( + {FieldValue::Boolean(true), FieldValue::Integer(123)}); + EXPECT_EQ(Type::kArray, value.type()); + const std::vector& array = value.array_value(); + EXPECT_EQ(2, array.size()); + EXPECT_EQ(true, array[0].boolean_value()); + EXPECT_EQ(123, array[1].integer_value()); +} + +TEST_F(FirestoreIntegrationTest, TestMapType) { + FieldValue value = + FieldValue::Map(MapFieldValue{{"Bool", FieldValue::Boolean(true)}, + {"Int", FieldValue::Integer(123)}}); + EXPECT_EQ(Type::kMap, value.type()); + MapFieldValue map = value.map_value(); + EXPECT_EQ(2, map.size()); + EXPECT_EQ(true, map["Bool"].boolean_value()); + EXPECT_EQ(123, map["Int"].integer_value()); +} + +TEST_F(FirestoreIntegrationTest, TestSentinelType) { + FieldValue delete_value = FieldValue::Delete(); + EXPECT_EQ(Type::kDelete, delete_value.type()); + + FieldValue server_timestamp_value = FieldValue::ServerTimestamp(); + EXPECT_EQ(Type::kServerTimestamp, server_timestamp_value.type()); + + std::vector array = {FieldValue::Boolean(true), + FieldValue::Integer(123)}; + FieldValue array_union = FieldValue::ArrayUnion(array); + EXPECT_EQ(Type::kArrayUnion, array_union.type()); + FieldValue array_remove = FieldValue::ArrayRemove(array); + EXPECT_EQ(Type::kArrayRemove, array_remove.type()); + + FieldValue increment_integer = FieldValue::Increment(1); + EXPECT_EQ(Type::kIncrementInteger, increment_integer.type()); + + FieldValue increment_double = FieldValue::Increment(1.0); + EXPECT_EQ(Type::kIncrementDouble, increment_double.type()); +} + +TEST_F(FirestoreIntegrationTest, TestEquality) { + EXPECT_EQ(FieldValue::Null(), FieldValue::Null()); + EXPECT_EQ(FieldValue::Boolean(true), FieldValue::Boolean(true)); + EXPECT_EQ(FieldValue::Integer(123), FieldValue::Integer(123)); + EXPECT_EQ(FieldValue::Double(456.0), FieldValue::Double(456.0)); + EXPECT_EQ(FieldValue::String("foo"), FieldValue::String("foo")); + + EXPECT_EQ(FieldValue::Timestamp({123, 456}), + FieldValue::Timestamp({123, 456})); + + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + EXPECT_EQ(FieldValue::Blob(blob, sizeof(blob)), + FieldValue::Blob(blob, sizeof(blob))); + + EXPECT_EQ(FieldValue::GeoPoint({43, 80}), FieldValue::GeoPoint({43, 80})); + + EXPECT_EQ( + FieldValue::Array({FieldValue::Integer(3), FieldValue::Double(4.0)}), + FieldValue::Array({FieldValue::Integer(3), FieldValue::Double(4.0)})); + + EXPECT_EQ(FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(3)}}), + FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(3)}})); + + EXPECT_EQ(FieldValue::Delete(), FieldValue::Delete()); + EXPECT_EQ(FieldValue::ServerTimestamp(), FieldValue::ServerTimestamp()); + // TODO(varconst): make this work on Android, or remove the tests below. + // EXPECT_EQ(FieldValue::ArrayUnion({FieldValue::Null()}), + // FieldValue::ArrayUnion({FieldValue::Null()})); + // EXPECT_EQ(FieldValue::ArrayRemove({FieldValue::Null()}), + // FieldValue::ArrayRemove({FieldValue::Null()})); +} + +TEST_F(FirestoreIntegrationTest, TestInequality) { + EXPECT_NE(FieldValue::Boolean(false), FieldValue::Boolean(true)); + EXPECT_NE(FieldValue::Integer(123), FieldValue::Integer(456)); + EXPECT_NE(FieldValue::Double(123.0), FieldValue::Double(456.0)); + EXPECT_NE(FieldValue::String("foo"), FieldValue::String("bar")); + + EXPECT_NE(FieldValue::Timestamp({123, 456}), + FieldValue::Timestamp({789, 123})); + + uint8_t blob1[] = "( ͡° ͜ʖ ͡°)"; + uint8_t blob2[] = "___"; + EXPECT_NE(FieldValue::Blob(blob1, sizeof(blob2)), + FieldValue::Blob(blob2, sizeof(blob2))); + + EXPECT_NE(FieldValue::GeoPoint({43, 80}), FieldValue::GeoPoint({12, 34})); + + EXPECT_NE( + FieldValue::Array({FieldValue::Integer(3), FieldValue::Double(4.0)}), + FieldValue::Array({FieldValue::Integer(5), FieldValue::Double(4.0)})); + + EXPECT_NE(FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(3)}}), + FieldValue::Map(MapFieldValue{{"foo", FieldValue::Integer(4)}})); + + EXPECT_NE(FieldValue::Delete(), FieldValue::ServerTimestamp()); + EXPECT_NE(FieldValue::ArrayUnion({FieldValue::Null()}), + FieldValue::ArrayUnion({FieldValue::Boolean(false)})); + EXPECT_NE(FieldValue::ArrayRemove({FieldValue::Null()}), + FieldValue::ArrayRemove({FieldValue::Boolean(false)})); +} + +TEST_F(FirestoreIntegrationTest, TestInequalityDueToDifferentTypes) { + EXPECT_NE(FieldValue::Null(), FieldValue::Delete()); + EXPECT_NE(FieldValue::Integer(1), FieldValue::Boolean(true)); + EXPECT_NE(FieldValue::Integer(123), FieldValue::Double(123)); + EXPECT_NE(FieldValue::ArrayUnion({FieldValue::Null()}), + FieldValue::ArrayRemove({FieldValue::Null()})); + EXPECT_NE(FieldValue::Array({FieldValue::Null()}), + FieldValue::ArrayRemove({FieldValue::Null()})); + // Fully exhaustive check seems overkill, just check the types that are known + // to have the same (or very similar) representation. +} + +TEST_F(FirestoreIntegrationTest, TestToString) { + EXPECT_EQ("", FieldValue().ToString()); + + EXPECT_EQ("null", FieldValue::Null().ToString()); + EXPECT_EQ("true", FieldValue::Boolean(true).ToString()); + EXPECT_EQ("123", FieldValue::Integer(123L).ToString()); + EXPECT_EQ("3.14", FieldValue::Double(3.14).ToString()); + EXPECT_EQ("Timestamp(seconds=12345, nanoseconds=54321)", + FieldValue::Timestamp({12345, 54321}).ToString()); + EXPECT_EQ("'hello'", FieldValue::String("hello").ToString()); + uint8_t blob[] = "( ͡° ͜ʖ ͡°)"; + EXPECT_EQ("Blob(28 20 cd a1 c2 b0 20 cd 9c ca 96 20 cd a1 c2 b0 29 00)", + FieldValue::Blob(blob, sizeof(blob)).ToString()); + EXPECT_EQ("GeoPoint(latitude=43, longitude=80)", + FieldValue::GeoPoint({43, 80}).ToString()); + + EXPECT_EQ("DocumentReference(invalid)", FieldValue::Reference({}).ToString()); + + EXPECT_EQ("[]", FieldValue::Array({}).ToString()); + EXPECT_EQ("[null]", FieldValue::Array({FieldValue::Null()}).ToString()); + EXPECT_EQ("[null, true, 1]", + FieldValue::Array({FieldValue::Null(), FieldValue::Boolean(true), + FieldValue::Integer(1)}) + .ToString()); + // TODO(b/150016438): uncomment this case (fails on Android). + // EXPECT_EQ("[]", FieldValue::Array({FieldValue()}).ToString()); + + EXPECT_EQ("{}", FieldValue::Map({}).ToString()); + // TODO(b/150016438): uncomment this case (fails on Android). + // EXPECT_EQ("{bad: }", FieldValue::Map({ + // {"bad", + // FieldValue()}, + // }) + // .ToString()); + EXPECT_EQ("{Null: null}", FieldValue::Map({ + {"Null", FieldValue::Null()}, + }) + .ToString()); + // Note: because the map is unordered, it's hard to check the case where a map + // has more than one element. + + EXPECT_EQ("FieldValue::Delete()", FieldValue::Delete().ToString()); + EXPECT_EQ("FieldValue::ServerTimestamp()", + FieldValue::ServerTimestamp().ToString()); + EXPECT_EQ("FieldValue::ArrayUnion()", + FieldValue::ArrayUnion({FieldValue::Null()}).ToString()); + EXPECT_EQ("FieldValue::ArrayRemove()", + FieldValue::ArrayRemove({FieldValue::Null()}).ToString()); + + EXPECT_EQ("FieldValue::Increment()", FieldValue::Increment(1).ToString()); + EXPECT_EQ("FieldValue::Increment()", FieldValue::Increment(1.0).ToString()); +} + +TEST_F(FirestoreIntegrationTest, TestIncrementChoosesTheCorrectType) { + // Signed integers + // NOLINTNEXTLINE -- exact integer width doesn't matter. + short foo = 1; + EXPECT_EQ(FieldValue::Increment(foo).type(), Type::kIncrementInteger); + EXPECT_EQ(FieldValue::Increment(1).type(), Type::kIncrementInteger); + EXPECT_EQ(FieldValue::Increment(1L).type(), Type::kIncrementInteger); + // Note: using `long long` syntax to avoid go/lsc-long-long-literal. + // NOLINTNEXTLINE -- exact integer width doesn't matter. + long long llfoo = 1; + EXPECT_EQ(FieldValue::Increment(llfoo).type(), Type::kIncrementInteger); + + // Unsigned integers + // NOLINTNEXTLINE -- exact integer width doesn't matter. + unsigned short ufoo = 1; + EXPECT_EQ(FieldValue::Increment(ufoo).type(), Type::kIncrementInteger); + EXPECT_EQ(FieldValue::Increment(1U).type(), Type::kIncrementInteger); + + // Floating point + EXPECT_EQ(FieldValue::Increment(1.0f).type(), Type::kIncrementDouble); + EXPECT_EQ(FieldValue::Increment(1.0).type(), Type::kIncrementDouble); + + // The statements below shouldn't compile (uncomment to check). + + // Types that would lead to truncation: + // EXPECT_EQ(FieldValue::Increment(1UL).type(), Type::kIncrementInteger); + // unsigned long long ullfoo = 1; + // EXPECT_EQ(FieldValue::Increment(ullfoo).type(), Type::kIncrementInteger); + // EXPECT_EQ(FieldValue::Increment(1.0L).type(), Type::kIncrementDouble); + + // Inapplicable types: + // EXPECT_EQ(FieldValue::Increment(true).type(), Type::kIncrementInteger); + // EXPECT_EQ(FieldValue::Increment('a').type(), Type::kIncrementInteger); + // EXPECT_EQ(FieldValue::Increment("abc").type(), Type::kIncrementInteger); +} + +#endif // !defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/fields_test.cc b/firestore/src/tests/fields_test.cc new file mode 100644 index 0000000000..76e6c3bcce --- /dev/null +++ b/firestore/src/tests/fields_test.cc @@ -0,0 +1,229 @@ +#include +#include +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/FieldsTest.java +// except we do not port the tests for legacy timestamp behavior. C++ SDK does +// not support the legacy timestamp behavior. + +namespace firebase { +namespace firestore { + +class FieldsTest : public FirestoreIntegrationTest { + protected: + /** + * Creates test data with nested fields. + */ + MapFieldValue NestedData(int number) { + char buffer[32]; + MapFieldValue result; + + snprintf(buffer, sizeof(buffer), "room %d", number); + result["name"] = FieldValue::String(buffer); + + MapFieldValue nested; + nested["createdAt"] = FieldValue::Integer(number); + MapFieldValue deep_nested; + snprintf(buffer, sizeof(buffer), "deep-field-%d", number); + deep_nested["field"] = FieldValue::String(buffer); + nested["deep"] = FieldValue::Map(deep_nested); + result["metadata"] = FieldValue::Map(nested); + + return result; + } + + /** + * Creates test data with special characters in field names. Datastore + * currently prohibits mixing nested data with special characters so tests + * that use this data must be separate. + */ + MapFieldValue DottedData(int number) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "field %d", number); + + return {{"a", FieldValue::String(buffer)}, + {"b.dot", FieldValue::Integer(number)}, + {"c\\slash", FieldValue::Integer(number)}}; + } + + /** + * Creates test data with Timestamp. + */ + MapFieldValue DataWithTimestamp(Timestamp timestamp) { + return { + {"timestamp", FieldValue::Timestamp(timestamp)}, + {"nested", + FieldValue::Map({{"timestamp2", FieldValue::Timestamp(timestamp)}})}}; + } +}; + +TEST_F(FieldsTest, TestNestedFieldsCanBeWrittenWithSet) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + EXPECT_THAT(ReadDocument(doc).GetData(), testing::ContainerEq(NestedData(1))); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeReadDirectly) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + DocumentSnapshot snapshot = ReadDocument(doc); + + MapFieldValue expected = NestedData(1); + EXPECT_EQ(expected["name"].string_value(), + snapshot.Get("name").string_value()); + EXPECT_EQ(expected["metadata"].map_value(), + snapshot.Get("metadata").map_value()); + EXPECT_EQ(expected["metadata"] + .map_value()["deep"] + .map_value()["field"] + .string_value(), + snapshot.Get("metadata.deep.field").string_value()); + EXPECT_FALSE(snapshot.Get("metadata.nofield").is_valid()); + EXPECT_FALSE(snapshot.Get("nometadata.nofield").is_valid()); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeReadDirectlyViaFieldPath) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + DocumentSnapshot snapshot = ReadDocument(doc); + + MapFieldValue expected = NestedData(1); + EXPECT_EQ(expected["name"].string_value(), + snapshot.Get(FieldPath{"name"}).string_value()); + EXPECT_EQ(expected["metadata"].map_value(), + snapshot.Get(FieldPath{"metadata"}).map_value()); + EXPECT_EQ( + expected["metadata"] + .map_value()["deep"] + .map_value()["field"] + .string_value(), + snapshot.Get(FieldPath{"metadata", "deep", "field"}).string_value()); + EXPECT_FALSE(snapshot.Get(FieldPath{"metadata", "nofield"}).is_valid()); + EXPECT_FALSE(snapshot.Get(FieldPath{"nometadata", "nofield"}).is_valid()); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeUpdated) { + DocumentReference doc = Document(); + WriteDocument(doc, NestedData(1)); + UpdateDocument( + doc, MapFieldValue{{"metadata.deep.field", FieldValue::Integer(100)}, + {"metadata.added", FieldValue::Integer(200)}}); + EXPECT_THAT(ReadDocument(doc).GetData(), + testing::ContainerEq(MapFieldValue( + {{"name", FieldValue::String("room 1")}, + {"metadata", + FieldValue::Map( + {{"createdAt", FieldValue::Integer(1)}, + {"deep", FieldValue::Map( + {{"field", FieldValue::Integer(100)}})}, + {"added", FieldValue::Integer(200)}})}}))); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeUsedInQueryFilters) { + CollectionReference collection = Collection( + {{"1", NestedData(300)}, {"2", NestedData(100)}, {"3", NestedData(200)}}); + QuerySnapshot snapshot = ReadDocuments(collection.WhereGreaterThanOrEqualTo( + "metadata.createdAt", FieldValue::Integer(200))); + // inequality adds implicit sort on field + EXPECT_THAT(QuerySnapshotToValues(snapshot), + testing::ElementsAre(NestedData(200), NestedData(300))); +} + +TEST_F(FieldsTest, TestNestedFieldsCanBeUsedInOrderBy) { + CollectionReference collection = Collection( + {{"1", NestedData(300)}, {"2", NestedData(100)}, {"3", NestedData(200)}}); + QuerySnapshot snapshot = + ReadDocuments(collection.OrderBy("metadata.createdAt")); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(NestedData(100), NestedData(200), NestedData(300))); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeWrittenWithSet) { + DocumentReference doc = Document(); + WriteDocument(doc, DottedData(1)); + EXPECT_EQ(DottedData(1), ReadDocument(doc).GetData()); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeReadDirectly) { + DocumentReference doc = Document(); + WriteDocument(doc, DottedData(1)); + DocumentSnapshot snapshot = ReadDocument(doc); + + MapFieldValue expected = DottedData(1); + EXPECT_EQ(expected["a"].string_value(), snapshot.Get("a").string_value()); + EXPECT_EQ(expected["b.dot"].integer_value(), + snapshot.GetData()["b.dot"].integer_value()); + EXPECT_EQ(expected["c\\slash"].integer_value(), + snapshot.GetData()["c\\slash"].integer_value()); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeUpdated) { + DocumentReference doc = Document(); + WriteDocument(doc, DottedData(1)); + UpdateDocument(doc, MapFieldPathValue{ + {FieldPath{"b.dot"}, FieldValue::Integer(100)}, + {FieldPath{"c\\slash"}, FieldValue::Integer(200)}}); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(ReadDocument(doc).GetData(), + testing::ContainerEq( + MapFieldValue({{"a", FieldValue::String("field 1")}, + {"b.dot", FieldValue::Integer(100)}, + {"c\\slash", FieldValue::Integer(200)}}))); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeUsedInQueryFilters) { + CollectionReference collection = Collection( + {{"1", DottedData(300)}, {"2", DottedData(100)}, {"3", DottedData(200)}}); + QuerySnapshot snapshot = ReadDocuments(collection.WhereGreaterThanOrEqualTo( + FieldPath{"b.dot"}, FieldValue::Integer(200))); + // inequality adds implicit sort on field + EXPECT_THAT(QuerySnapshotToValues(snapshot), + testing::ElementsAre(DottedData(200), DottedData(300))); +} + +TEST_F(FieldsTest, TestFieldsWithSpecialCharsCanBeUsedInOrderBy) { + CollectionReference collection = Collection( + {{"1", DottedData(300)}, {"2", DottedData(100)}, {"3", DottedData(200)}}); + QuerySnapshot snapshot = + ReadDocuments(collection.OrderBy(FieldPath{"b.dot"})); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(DottedData(100), DottedData(200), DottedData(300))); +} + +TEST_F(FieldsTest, TestTimestampsInSnapshots) { + Timestamp original_timestamp{100, 123456789}; + // Timestamps are currently truncated to microseconds after being written to + // the database. + Timestamp truncated_timestamp{100, 123456000}; + + DocumentReference doc = Document(); + WriteDocument(doc, DataWithTimestamp(original_timestamp)); + DocumentSnapshot snapshot = ReadDocument(doc); + MapFieldValue data = snapshot.GetData(); + + Timestamp timestamp_from_snapshot = + snapshot.Get("timestamp").timestamp_value(); + Timestamp timestamp_from_data = data["timestamp"].timestamp_value(); + EXPECT_EQ(truncated_timestamp, timestamp_from_data); + EXPECT_EQ(timestamp_from_snapshot, timestamp_from_data); + + timestamp_from_snapshot = snapshot.Get("nested.timestamp2").timestamp_value(); + timestamp_from_data = + data["nested"].map_value()["timestamp2"].timestamp_value(); + EXPECT_EQ(truncated_timestamp, timestamp_from_data); + EXPECT_EQ(timestamp_from_snapshot, timestamp_from_data); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/firestore_integration_test.cc b/firestore/src/tests/firestore_integration_test.cc new file mode 100644 index 0000000000..7462004b94 --- /dev/null +++ b/firestore/src/tests/firestore_integration_test.cc @@ -0,0 +1,219 @@ +#include "firestore/src/tests/firestore_integration_test.h" + +#include +#include +#include + +#include "absl/strings/ascii.h" +#include "Firestore/core/src/util/autoid.h" + +namespace firebase { +namespace firestore { + +namespace { +// name of FirebaseApp to use for bootstrapping data into Firestore. We use a +// non-default app to avoid data ending up in the cache before tests run. +static const char* kBootstrapAppName = "bootstrap"; + +void Release(Firestore* firestore) { + if (firestore == nullptr) { + return; + } + + App* app = firestore->app(); + delete firestore; + delete app; +} + +// Set Firestore up to use Firestore Emulator if it can be found. +void LocateEmulator(Firestore* db) { + // iOS and Android pass emulator address differently, iOS writes it to a + // temp file, but there is no equivalent to `/tmp/` for Android, so it + // uses an environment variable instead. + // TODO(wuandy): See if we can use environment variable for iOS as well? + std::ifstream ifs("/tmp/emulator_address"); + std::stringstream buffer; + buffer << ifs.rdbuf(); + std::string address; + if (ifs.good()) { + address = buffer.str(); + } else if (std::getenv("FIRESTORE_EMULATOR_HOST")) { + address = std::getenv("FIRESTORE_EMULATOR_HOST"); + } + + absl::StripAsciiWhitespace(&address); + if (!address.empty()) { + auto settings = db->settings(); + settings.set_host(address); + // Emulator does not support ssl yet. + settings.set_ssl_enabled(false); + db->set_settings(settings); + } +} + +} // anonymous namespace + +FirestoreIntegrationTest::FirestoreIntegrationTest() { + // Allocate the default Firestore eagerly. + CachedFirestore(kDefaultAppName); + Firestore::set_log_level(LogLevel::kLogLevelDebug); +} + +FirestoreIntegrationTest::~FirestoreIntegrationTest() { + for (auto named_firestore : firestores_) { + Await(named_firestore.second->Terminate()); + Release(named_firestore.second); + firestores_[named_firestore.first] = nullptr; + } +} + +Firestore* FirestoreIntegrationTest::CachedFirestore( + const std::string& name) const { + if (firestores_.count(name) > 0) { + return firestores_[name]; + } + + // Make sure different unit tests don't try to create an app with the same + // name, because it's not supported by `firebase::App` (the default app is an + // exception and will be recreated). + static int counter = 0; + std::string app_name = + name == kDefaultAppName ? name : name + std::to_string(counter++); + Firestore* db = CreateFirestore(app_name); + + firestores_[name] = db; + return db; +} + +Firestore* FirestoreIntegrationTest::CreateFirestore() const { + static int app_number = 0; + std::string app_name = "app_for_testing_"; + app_name += std::to_string(app_number++); + return CreateFirestore(app_name); +} + +Firestore* FirestoreIntegrationTest::CreateFirestore( + const std::string& app_name) const { + App* app = GetApp(app_name.c_str()); + Firestore* db = new Firestore(CreateTestFirestoreInternal(app)); + + LocateEmulator(db); + InitializeFirestore(db); + return db; +} + +void FirestoreIntegrationTest::DeleteFirestore(const std::string& name) { + auto found = firestores_.find(name); + FIREBASE_ASSERT_MESSAGE( + found != firestores_.end(), + "Couldn't find Firestore corresponding to app name '%s'", name.c_str()); + + TerminateAndRelease(found->second); + firestores_.erase(found); +} + +CollectionReference FirestoreIntegrationTest::Collection() const { + return firestore()->Collection(util::CreateAutoId()); +} + +CollectionReference FirestoreIntegrationTest::Collection( + const std::string& name_prefix) const { + return firestore()->Collection(name_prefix + "_" + util::CreateAutoId()); +} + +CollectionReference FirestoreIntegrationTest::Collection( + const std::map& docs) const { + CollectionReference result = Collection(); + WriteDocuments(CachedFirestore(kBootstrapAppName)->Collection(result.path()), + docs); + return result; +} + +std::string FirestoreIntegrationTest::DocumentPath() const { + return "test-collection/" + util::CreateAutoId(); +} + +DocumentReference FirestoreIntegrationTest::Document() const { + return firestore()->Document(DocumentPath()); +} + +void FirestoreIntegrationTest::WriteDocument(DocumentReference reference, + const MapFieldValue& data) const { + Future future = reference.Set(data); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; +} + +void FirestoreIntegrationTest::WriteDocuments( + CollectionReference reference, + const std::map& data) const { + for (const auto& kv : data) { + WriteDocument(reference.Document(kv.first), kv.second); + } +} + +DocumentSnapshot FirestoreIntegrationTest::ReadDocument( + const DocumentReference& reference) const { + Future future = reference.Get(); + const DocumentSnapshot* result = Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + EXPECT_NE(nullptr, result) << DescribeFailedFuture(future) << std::endl; + return *result; +} + +QuerySnapshot FirestoreIntegrationTest::ReadDocuments( + const Query& reference) const { + Future future = reference.Get(); + const QuerySnapshot* result = Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + EXPECT_NE(nullptr, result) << DescribeFailedFuture(future) << std::endl; + return *result; +} + +void FirestoreIntegrationTest::DeleteDocument( + DocumentReference reference) const { + Future future = reference.Delete(); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; +} + +std::vector FirestoreIntegrationTest::QuerySnapshotToIds( + const QuerySnapshot& snapshot) const { + std::vector result; + for (const DocumentSnapshot& doc : snapshot.documents()) { + result.push_back(doc.id()); + } + return result; +} + +std::vector FirestoreIntegrationTest::QuerySnapshotToValues( + const QuerySnapshot& snapshot) const { + std::vector result; + for (const DocumentSnapshot& doc : snapshot.documents()) { + result.push_back(doc.GetData()); + } + return result; +} + +/* static */ +void FirestoreIntegrationTest::Await(const Future& future) { + while (future.status() == FutureStatus::kFutureStatusPending) { + if (ProcessEvents(kCheckIntervalMillis)) { + std::cout << "WARNING: app received an event requesting exit." + << std::endl; + break; + } + } +} + +void FirestoreIntegrationTest::TerminateAndRelease(Firestore* firestore) { + Await(firestore->Terminate()); + Release(firestore); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/firestore_integration_test.h b/firestore/src/tests/firestore_integration_test.h new file mode 100644 index 0000000000..e22bd38db2 --- /dev/null +++ b/firestore/src/tests/firestore_integration_test.h @@ -0,0 +1,275 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_FIRESTORE_INTEGRATION_TEST_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_FIRESTORE_INTEGRATION_TEST_H_ + +#include +#include +#include +#include +#include + +#include "app/src/assert.h" +#include "app/src/include/firebase/internal/common.h" +#include "app/src/mutex.h" +#include "firestore/src/include/firebase/firestore.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +// The interval between checks for future completion. +const int kCheckIntervalMillis = 100; + +// The timeout of waiting for a Future or a listener. +const int kTimeOutMillis = 15000; + +FirestoreInternal* CreateTestFirestoreInternal(App* app); +void InitializeFirestore(Firestore* instance); + +App* GetApp(); +App* GetApp(const char* name); +bool ProcessEvents(int msec); + +template +class EventAccumulator; + +// An EventListener class for writing tests. This listener counts the number of +// events as well as keeps track of the last result. +template +class TestEventListener : public EventListener { + public: + explicit TestEventListener(std::string name) : name_(std::move(name)) {} + + ~TestEventListener() override {} + + void OnEvent(const T& value, Error error) override { + if (print_debug_info_) { + std::cout << "TestEventListener got: "; + if (error == Error::kErrorOk) { + std::cout << &value + << " from_cache:" << value.metadata().is_from_cache() + << " has_pending_write:" + << value.metadata().has_pending_writes() << std::endl; + } else { + std::cout << "error:" << error << std::endl; + } + } + + event_count_++; + if (error != Error::kErrorOk) { + std::cerr << "ERROR: EventListener " << name_ << " got " << error + << std::endl; + if (first_error_ == Error::kErrorOk) { + first_error_ = error; + } + } + MutexLock lock(mutex_); + last_result_.push_back(value); + } + + int event_count() const { return event_count_; } + + const T& last_result(int i = 0) { + FIREBASE_ASSERT(i >= 0 && i < last_result_.size()); + MutexLock lock(mutex_); + return last_result_[last_result_.size() - 1 - i]; + } + + // Hides the STLPort-related quirk that `AddSnapshotListener` has different + // signatures depending on whether `std::function` is available. + template + ListenerRegistration AttachTo( + U* ref, MetadataChanges metadata_changes = MetadataChanges::kExclude) { +#if defined(FIREBASE_USE_STD_FUNCTION) + return ref->AddSnapshotListener( + metadata_changes, + [this](const T& result, Error error) { OnEvent(result, error); }); +#else + return ref->AddSnapshotListener(metadata_changes, this); +#endif + } + + Error first_error() { return first_error_; } + + // Set this to true to print more details for each arrived event for debug. + void set_print_debug_info(bool value) { print_debug_info_ = value; } + + private: + friend class EventAccumulator; + + std::string name_; + int event_count_ = 0; + + // We may want the last N result. So we store all in a vector in the order + // they arrived. + std::vector last_result_; + // We add a mutex to protect the calls to push_back, which is not thread-safe. + // Marked as `mutable` so that const functions can still be protected. + mutable Mutex mutex_; + + // We generally only check to see if there is any error. So we only store the + // first non-OK error, if any. + Error first_error_ = Error::kErrorOk; + + bool print_debug_info_ = false; +}; + +// Base class for Firestore integration tests. +// Note it keeps a cached of created Firestore instances, and is thread-unsafe. +class FirestoreIntegrationTest : public testing::Test { + friend class TransactionTester; + + public: + FirestoreIntegrationTest(); + FirestoreIntegrationTest(const FirestoreIntegrationTest&) = delete; + FirestoreIntegrationTest(FirestoreIntegrationTest&&) = delete; + ~FirestoreIntegrationTest() override; + + FirestoreIntegrationTest& operator=(const FirestoreIntegrationTest&) = delete; + FirestoreIntegrationTest& operator=(FirestoreIntegrationTest&&) = delete; + + protected: + App* app() { return firestore()->app(); } + + Firestore* firestore() const { return CachedFirestore(kDefaultAppName); } + + // If no Firestore instance is registered under the name, creates a new + // instance in order to have multiple Firestore clients for testing. + // Otherwise, returns the registered Firestore instance. + Firestore* CachedFirestore(const std::string& name) const; + + // Blocks until the Firestore instance corresponding to the given app name + // shuts down, deletes the instance and removes the pointer to it from the + // cache. Asserts that a Firestore instance with the given name does exist. + void DeleteFirestore(const std::string& name = kDefaultAppName); + + // Return a reference to the collection with auto-generated id. + CollectionReference Collection() const; + + // Return a reference to a collection with the path constructed by appending a + // unique id to the given name. + CollectionReference Collection(const std::string& name_prefix) const; + + // Return a reference to the collection with given content. + CollectionReference Collection( + const std::map& docs) const; + + // Return an auto-generated document path under collection "test-collection". + std::string DocumentPath() const; + + // Return a reference to the document with auto-generated id. + DocumentReference Document() const; + + // Write to the specified document and wait for the write to complete. + void WriteDocument(DocumentReference reference, + const MapFieldValue& data) const; + + // Write to the specified documents to a collection and wait for completion. + void WriteDocuments(CollectionReference reference, + const std::map& data) const; + + // Update the specified document and wait for the update to complete. + template + void UpdateDocument(DocumentReference reference, const MapType& data) const { + Future future = reference.Update(data); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + } + + // Read the specified document. + DocumentSnapshot ReadDocument(const DocumentReference& reference) const; + + // Read documents in the specified collection / query. + QuerySnapshot ReadDocuments(const Query& reference) const; + + // Delete the specified document. + void DeleteDocument(DocumentReference reference) const; + + // Convert a QuerySnapshot to the id of each document. + std::vector QuerySnapshotToIds( + const QuerySnapshot& snapshot) const; + + // Convert a QuerySnapshot to the contents of each document. + std::vector QuerySnapshotToValues( + const QuerySnapshot& snapshot) const; + + // TODO(zxu): add a helper function to block on signal. + + // A helper function to block until the future completes. + template + static const T* Await(const Future& future) { + // Instead of getting a clock, we count the cycles instead. + int cycles = kTimeOutMillis / kCheckIntervalMillis; + while (future.status() == FutureStatus::kFutureStatusPending && + cycles > 0) { + if (ProcessEvents(kCheckIntervalMillis)) { + std::cout << "WARNING: app receives an event requesting exit." + << std::endl; + return nullptr; + } + --cycles; + } + EXPECT_GT(cycles, 0) << "Waiting future timed out."; + if (future.status() == FutureStatus::kFutureStatusComplete) { + if (future.result() == nullptr) { + std::cout << "WARNING: Future failed. Error code " << future.error() + << ", message " << future.error_message() << std::endl; + } + } else { + std::cout << "WARNING: Future is not completed." << std::endl; + } + return future.result(); + } + + static void Await(const Future& future); + + // A helper function to block until there is at least n event. + template + static void Await(const TestEventListener& listener, int n = 1) { + // Instead of getting a clock, we count the cycles instead. + int cycles = kTimeOutMillis / kCheckIntervalMillis; + while (listener.event_count() < n && cycles > 0) { + if (ProcessEvents(kCheckIntervalMillis)) { + std::cout << "WARNING: app receives an event requesting exit." + << std::endl; + return; + } + --cycles; + } + EXPECT_GT(cycles, 0) << "Waiting listener timed out."; + } + + template + static std::string DescribeFailedFuture(const Future& future) { + return "WARNING: Future failed. Error code " + + std::to_string(future.error()) + ", message " + + future.error_message(); + } + + // Creates a new Firestore instance, without any caching, using a uniquely- + // generated app_name. + Firestore* CreateFirestore() const; + // Creates a new Firestore instance, without any caching, using the given + // app_name. + Firestore* CreateFirestore(const std::string& app_name) const; + + void DisableNetwork() { Await(firestore()->DisableNetwork()); } + + void EnableNetwork() { Await(firestore()->EnableNetwork()); } + + private: + template + friend class EventAccumulator; + + // Blocks until the given Firestore instance terminates, deletes the instance + // and removes the pointer to it from the cache. + void TerminateAndRelease(Firestore* firestore); + + // The Firestore instance cache. + mutable std::map firestores_; +}; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_FIRESTORE_INTEGRATION_TEST_H_ diff --git a/firestore/src/tests/firestore_test.cc b/firestore/src/tests/firestore_test.cc new file mode 100644 index 0000000000..8a14257761 --- /dev/null +++ b/firestore/src/tests/firestore_test.cc @@ -0,0 +1,1334 @@ +#if !defined(__ANDROID__) +#include // NOLINT(build/c++11) +#endif + +#if !defined(FIRESTORE_STUB_BUILD) +#include "app/src/semaphore.h" +#endif +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/util_android.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/FirestoreTest.java +// Some test cases are named differently between iOS and Android. Here we choose +// the most descriptive names. + +namespace firebase { +namespace firestore { + +TEST_F(FirestoreIntegrationTest, GetInstance) { + // Create App. + App* app = this->app(); + EXPECT_NE(nullptr, app); + + // Get an instance. + InitResult result; + Firestore* instance = Firestore::GetInstance(app, &result); + EXPECT_EQ(kInitResultSuccess, result); + EXPECT_NE(nullptr, instance); + EXPECT_EQ(app, instance->app()); +} + +// Sanity test for stubs. +TEST_F(FirestoreIntegrationTest, TestCanCreateCollectionAndDocumentReferences) { + ASSERT_NO_THROW({ + Firestore* db = firestore(); + CollectionReference c = db->Collection("a/b/c").Document("d").Parent(); + DocumentReference d = db->Document("a/b").Collection("c/d/e").Parent(); + + CollectionReference(c).Document(); + DocumentReference(d).Parent(); + + CollectionReference(std::move(c)).Document(); + DocumentReference(std::move(d)).Parent(); + }); +} + +#if defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestStubsReturnFailedFutures) { + Firestore* db = firestore(); + Future future = db->EnableNetwork(); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorFailedPrecondition, future.error()); + + future = db->Document("foo/bar").Set( + MapFieldValue{{"foo", FieldValue::String("bar")}}); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorFailedPrecondition, future.error()); +} + +#else // defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestCanUpdateAnExistingDocument) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Update( + MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner.email", FieldValue::String("new@xyz.com")}})); + DocumentSnapshot doc = ReadDocument(document); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("new@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanUpdateAnUnknownDocument) { + DocumentReference writer_reference = + CachedFirestore("writer")->Collection("collection").Document(); + DocumentReference reader_reference = CachedFirestore("reader") + ->Collection("collection") + .Document(writer_reference.id()); + Await(writer_reference.Set(MapFieldValue{{"a", FieldValue::String("a")}})); + Await(reader_reference.Update(MapFieldValue{{"b", FieldValue::String("b")}})); + + DocumentSnapshot writer_snapshot = + *Await(writer_reference.Get(Source::kCache)); + EXPECT_TRUE(writer_snapshot.exists()); + EXPECT_THAT( + writer_snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::String("a")}})); + EXPECT_TRUE(writer_snapshot.metadata().is_from_cache()); + + Future future = reader_reference.Get(Source::kCache); + Await(future); + EXPECT_EQ(Error::kErrorUnavailable, future.error()); + + writer_snapshot = ReadDocument(writer_reference); + EXPECT_THAT(writer_snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("a")}, + {"b", FieldValue::String("b")}})); + EXPECT_FALSE(writer_snapshot.metadata().is_from_cache()); + DocumentSnapshot reader_snapshot = ReadDocument(reader_reference); + EXPECT_THAT(reader_snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("a")}, + {"b", FieldValue::String("b")}})); + EXPECT_FALSE(reader_snapshot.metadata().is_from_cache()); +} + +TEST_F(FirestoreIntegrationTest, TestCanOverwriteAnExistingDocumentUsingSet) { + DocumentReference document = Collection("rooms").Document(); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set(MapFieldValue{ + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}})}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}})}})); +} + +TEST_F(FirestoreIntegrationTest, + TestCanMergeDataWithAnExistingDocumentUsingSet) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{ + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}})}}, + SetOptions::Merge())); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"updated", FieldValue::Boolean(true)}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanMergeServerTimestamps) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{{"untouched", FieldValue::Boolean(true)}})); + Await(document.Set( + MapFieldValue{{"time", FieldValue::ServerTimestamp()}, + {"nested", FieldValue::Map( + {{"time", FieldValue::ServerTimestamp()}})}}, + SetOptions::Merge())); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.Get("untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("time").is_timestamp()); + EXPECT_TRUE(snapshot.Get("nested.time").is_timestamp()); +} + +TEST_F(FirestoreIntegrationTest, TestCanMergeEmptyObject) { + DocumentReference document = Document(); + EventAccumulator accumulator; + ListenerRegistration registration = + accumulator.listener()->AttachTo(&document); + accumulator.Await(); + + document.Set(MapFieldValue{}); + DocumentSnapshot snapshot = accumulator.Await(); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{})); + + Await(document.Set(MapFieldValue{{"a", FieldValue::Map({})}}, + SetOptions::MergeFields({"a"}))); + snapshot = accumulator.Await(); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Map({})}})); + + Await(document.Set(MapFieldValue{{"b", FieldValue::Map({})}}, + SetOptions::Merge())); + snapshot = accumulator.Await(); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Map({})}, + {"b", FieldValue::Map({})}})); + + snapshot = *Await(document.Get(Source::kServer)); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Map({})}, + {"b", FieldValue::Map({})}})); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestCanDeleteFieldUsingMerge) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.Get("untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("nested.untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("foo").is_valid()); + EXPECT_TRUE(snapshot.Get("nested.foo").is_valid()); + + Await(document.Set( + MapFieldValue{{"foo", FieldValue::Delete()}, + {"nested", FieldValue::Map(MapFieldValue{ + {"foo", FieldValue::Delete()}})}}, + SetOptions::Merge())); + snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.Get("untouched").boolean_value()); + EXPECT_TRUE(snapshot.Get("nested.untouched").boolean_value()); + EXPECT_FALSE(snapshot.Get("foo").is_valid()); + EXPECT_FALSE(snapshot.Get("nested.foo").is_valid()); +} + +TEST_F(FirestoreIntegrationTest, TestCanDeleteFieldUsingMergeFields) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}, + {"inner", FieldValue::Map({{"removed", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}})); + Await(document.Set( + MapFieldValue{ + {"foo", FieldValue::Delete()}, + {"inner", FieldValue::Map({{"foo", FieldValue::Delete()}})}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Delete()}, + {"foo", FieldValue::Delete()}})}}, + SetOptions::MergeFields({"foo", "inner", "nested.foo"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"inner", FieldValue::Map({})}, + {"nested", + FieldValue::Map({{"untouched", FieldValue::Boolean(true)}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanSetServerTimestampsUsingMergeFields) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}, + {"nested", FieldValue::Map({{"untouched", FieldValue::Boolean(true)}, + {"foo", FieldValue::String("bar")}})}})); + Await(document.Set( + MapFieldValue{ + {"foo", FieldValue::ServerTimestamp()}, + {"inner", FieldValue::Map({{"foo", FieldValue::ServerTimestamp()}})}, + {"nested", + FieldValue::Map({{"foo", FieldValue::ServerTimestamp()}})}}, + SetOptions::MergeFields({"foo", "inner", "nested.foo"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_TRUE(snapshot.exists()); + EXPECT_TRUE(snapshot.Get("foo").is_timestamp()); + EXPECT_TRUE(snapshot.Get("inner.foo").is_timestamp()); + EXPECT_TRUE(snapshot.Get("nested.foo").is_timestamp()); +} + +TEST_F(FirestoreIntegrationTest, TestMergeReplacesArrays) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"data", FieldValue::String("old")}, + {"topLevel", FieldValue::Array( + {FieldValue::String("old"), FieldValue::String("old")})}, + {"mapInArray", FieldValue::Array({FieldValue::Map( + {{"data", FieldValue::String("old")}})})}})); + Await(document.Set( + MapFieldValue{ + {"data", FieldValue::String("new")}, + {"topLevel", FieldValue::Array({FieldValue::String("new")})}, + {"mapInArray", FieldValue::Array({FieldValue::Map( + {{"data", FieldValue::String("new")}})})}}, + SetOptions::Merge())); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"untouched", FieldValue::Boolean(true)}, + {"data", FieldValue::String("new")}, + {"topLevel", FieldValue::Array({FieldValue::String("new")})}, + {"mapInArray", FieldValue::Array({FieldValue::Map( + {{"data", FieldValue::String("new")}})})}})); +} + +TEST_F(FirestoreIntegrationTest, + TestCanDeepMergeDataWithAnExistingDocumentUsingSet) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("old@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("new@xyz.com")}})}}, + SetOptions::MergeFieldPaths({{"desc"}, {"owner.data", "name"}}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner.data", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("old@xyz.com")}})}})); +} + +#if defined(__ANDROID__) +// TODO(b/136012313): iOS currently doesn't rethrow native exceptions as C++ +// exceptions. +TEST_F(FirestoreIntegrationTest, TestFieldMaskCannotContainMissingFields) { + DocumentReference document = Collection("rooms").Document(); + try { + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}}, + SetOptions::MergeFields({"desc", "owner"})); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Field 'owner' is specified in your field mask but not in your input " + "data.", + exception.what()); + } +} +#endif + +TEST_F(FirestoreIntegrationTest, TestFieldsNotInFieldMaskAreIgnored) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await( + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner", FieldValue::String("Sebastian")}}, + SetOptions::MergeFields({"desc"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestFieldDeletesNotInFieldMaskAreIgnored) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await( + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner", FieldValue::Delete()}}, + SetOptions::MergeFields({"desc"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestFieldTransformsNotInFieldMaskAreIgnored) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await( + document.Set(MapFieldValue{{"desc", FieldValue::String("NewDescription")}, + {"owner", FieldValue::ServerTimestamp()}}, + SetOptions::MergeFields({"desc"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanSetEmptyFieldMask) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{{"desc", FieldValue::String("NewDescription")}}, + SetOptions::MergeFields({}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanSpecifyFieldsMultipleTimesInFieldMask) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Set( + MapFieldValue{ + {"desc", FieldValue::String("NewDescription")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("new@new.com")}})}}, + SetOptions::MergeFields({"owner.name", "owner", "owner"}))); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("new@new.com")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanDeleteAFieldWithAnUpdate) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + Await(document.Update(MapFieldValue{{"owner.email", FieldValue::Delete()}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Jonny")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanUpdateFieldsWithDots) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{{"a.b", FieldValue::String("old")}, + {"c.d", FieldValue::String("old")}, + {"e.f", FieldValue::String("old")}})); + Await(document.Update({{FieldPath{"a.b"}, FieldValue::String("new")}})); + Await(document.Update({{FieldPath{"c.d"}, FieldValue::String("new")}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a.b", FieldValue::String("new")}, + {"c.d", FieldValue::String("new")}, + {"e.f", FieldValue::String("old")}})); +} + +TEST_F(FirestoreIntegrationTest, TestCanUpdateNestedFields) { + DocumentReference document = Collection("rooms").Document("eros"); + Await(document.Set(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("old")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("old")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); + Await(document.Update({{"a.b", FieldValue::String("new")}})); + Await(document.Update({{"c.d", FieldValue::String("new")}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("new")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("new")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); +} + +TEST_F(FirestoreIntegrationTest, TestDeleteDocument) { + DocumentReference document = Collection("rooms").Document("eros"); + WriteDocument(document, MapFieldValue{{"value", FieldValue::String("bar")}}); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"value", FieldValue::String("bar")}})); + + Await(document.Delete()); + snapshot = ReadDocument(document); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(FirestoreIntegrationTest, TestCannotUpdateNonexistentDocument) { + DocumentReference document = Collection("rooms").Document(); + Future future = + document.Update(MapFieldValue{{"owner", FieldValue::String("abc")}}); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(FirestoreIntegrationTest, TestCanRetrieveNonexistentDocument) { + DocumentReference document = Collection("rooms").Document(); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_FALSE(snapshot.exists()); + + TestEventListener listener{"for document"}; + ListenerRegistration registration = listener.AttachTo(&document); + Await(listener); + EXPECT_EQ(Error::kErrorOk, listener.first_error()); + EXPECT_FALSE(listener.last_result().exists()); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, + TestAddingToACollectionYieldsTheCorrectDocumentReference) { + DocumentReference document = Collection("rooms").Document(); + Await(document.Set(MapFieldValue{{"foo", FieldValue::Double(1.0)}})); + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::Double(1.0)}})); +} + +TEST_F(FirestoreIntegrationTest, + TestSnapshotsInSyncListenerFiresAfterListenersInSync) { + DocumentReference document = Collection("rooms").Document(); + Await(document.Set(MapFieldValue{{"foo", FieldValue::Double(1.0)}})); + std::vector events; + + class SnapshotTestEventListener : public TestEventListener { + public: + SnapshotTestEventListener(std::string name, + std::vector* events) + : TestEventListener(std::move(name)), events_(events) {} + + void OnEvent(const DocumentSnapshot& value, Error error) override { + TestEventListener::OnEvent(value, error); + events_->push_back("doc"); + } + + private: + std::vector* events_; + }; + SnapshotTestEventListener listener{"doc", &events}; + ListenerRegistration doc_registration = listener.AttachTo(&document); + // Wait for the initial event from the backend so that we know we'll get + // exactly one snapshot event for our local write below. + Await(listener); + EXPECT_EQ(1, events.size()); + events.clear(); + +#if defined(__APPLE__) + // TODO(varconst): the implementation of `Semaphore::Post()` on Apple + // platforms has a data race which may result in semaphore data being accessed + // on the listener thread after it was destroyed on the main thread. To work + // around this, use `std::promise`. + std::promise promise; +#else + Semaphore completed{0}; +#endif + +#if defined(FIREBASE_USE_STD_FUNCTION) + ListenerRegistration sync_registration = + firestore()->AddSnapshotsInSyncListener([&] { + events.push_back("snapshots-in-sync"); + if (events.size() == 3) { +#if defined(__APPLE__) + promise.set_value(); +#else + completed.Post(); +#endif + } + }); + +#else + class SyncEventListener : public EventListener { + public: + explicit SyncEventListener(std::vector* events, + Semaphore* completed) + : events_(events), completed_(completed) {} + + void OnEvent(Error) override { + events_->push_back("snapshots-in-sync"); + if (events.size() == 3) { + completed_->Post(); + } + } + + private: + std::vector* events_ = nullptr; + Semaphore* completed_ = nullptr; + }; + SyncEventListener sync_listener{&events, &completed}; + ListenerRegistration sync_registration = + firestore()->AddSnapshotsInSyncListener(sync_listener); +#endif // defined(FIREBASE_USE_STD_FUNCTION) + + Await(document.Set(MapFieldValue{{"foo", FieldValue::Double(3.0)}})); + // Wait for the snapshots-in-sync listener to fire afterwards. +#if defined(__APPLE__) + promise.get_future().wait(); +#else + completed.Wait(); +#endif + + // We should have an initial snapshots-in-sync event, then a snapshot event + // for set(), then another event to indicate we're in sync again. + EXPECT_EQ(events, std::vector( + {"snapshots-in-sync", "doc", "snapshots-in-sync"})); + doc_registration.Remove(); + sync_registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesAreValidatedOnClient) { + // NOTE: Failure cases are validated in ValidationTest. + CollectionReference collection = Collection(); + Query query = + collection.WhereGreaterThanOrEqualTo("x", FieldValue::Integer(32)); + // Same inequality field works; + query.WhereLessThanOrEqualTo("x", FieldValue::String("cat")); + // Equality on different field works; + query.WhereEqualTo("y", FieldValue::String("cat")); + // Array contains on different field works; + query.WhereArrayContains("y", FieldValue::String("cat")); + + // Ordering by inequality field succeeds. + query.OrderBy("x"); + collection.OrderBy("x").WhereGreaterThanOrEqualTo("x", + FieldValue::Integer(32)); + + // inequality same as first order by works + query.OrderBy("x").OrderBy("y"); + collection.OrderBy("x").OrderBy("y").WhereGreaterThanOrEqualTo( + "x", FieldValue::Integer(32)); + collection.OrderBy("x", Query::Direction::kDescending) + .WhereEqualTo("y", FieldValue::String("true")); + + // Equality different than orderBy works + collection.OrderBy("x").WhereEqualTo("y", FieldValue::String("cat")); + // Array contains different than orderBy works + collection.OrderBy("x").WhereArrayContains("y", FieldValue::String("cat")); +} + +// The test harness will generate Java JUnit test regardless whether this is +// inside a #if or not. So we move #if inside instead of enclose the whole case. +TEST_F(FirestoreIntegrationTest, TestListenCanBeCalledMultipleTimes) { + // Note: this test is flaky -- the test case may finish, triggering the + // destruction of Firestore, before the async callback finishes. +#if defined(FIREBASE_USE_STD_FUNCTION) + DocumentReference document = Collection("collection").Document(); + WriteDocument(document, MapFieldValue{{"foo", FieldValue::String("bar")}}); +#if defined(__APPLE__) + // TODO(varconst): the implementation of `Semaphore::Post()` on Apple + // platforms has a data race which may result in semaphore data being accessed + // on the listener thread after it was destroyed on the main thread. To work + // around this, use `std::promise`. + std::promise promise; +#else + Semaphore completed{0}; +#endif + DocumentSnapshot resulting_data; + document.AddSnapshotListener( + [&](const DocumentSnapshot& snapshot, Error error) { + EXPECT_EQ(Error::kErrorOk, error); + document.AddSnapshotListener( + [&](const DocumentSnapshot& snapshot, Error error) { + EXPECT_EQ(Error::kErrorOk, error); + resulting_data = snapshot; +#if defined(__APPLE__) + promise.set_value(); +#else + completed.Post(); +#endif + }); + }); +#if defined(__APPLE__) + promise.get_future().wait(); +#else + completed.Wait(); +#endif + EXPECT_THAT( + resulting_data.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsNonExistent) { + DocumentReference document = Collection("rooms").Document(); + TestEventListener listener("TestNonExistent"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener); + EXPECT_EQ(1, listener.event_count()); + EXPECT_EQ(Error::kErrorOk, listener.first_error()); + EXPECT_FALSE(listener.last_result().exists()); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForAdd) { + DocumentReference document = Collection("rooms").Document(); + TestEventListener listener("TestForAdd"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener); + EXPECT_FALSE(listener.last_result().exists()); + + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(1.0)}}); + Await(listener, 3); + DocumentSnapshot snapshot = listener.last_result(1); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + snapshot = listener.last_result(); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForChange) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForChange"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener); + DocumentSnapshot snapshot = listener.last_result(); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + UpdateDocument(document, MapFieldValue{{"a", FieldValue::Double(2.0)}}); + Await(listener, 3); + snapshot = listener.last_result(1); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + snapshot = listener.last_result(); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForDelete) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForDelete"); + ListenerRegistration registration = + listener.AttachTo(&document, MetadataChanges::kInclude); + Await(listener, 1); + DocumentSnapshot snapshot = listener.last_result(); + EXPECT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + DeleteDocument(document); + Await(listener, 2); + snapshot = listener.last_result(); + EXPECT_FALSE(snapshot.exists()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForAdd) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + TestEventListener listener("TestForCollectionAdd"); + ListenerRegistration registration = + listener.AttachTo(&collection, MetadataChanges::kInclude); + Await(listener); + EXPECT_EQ(0, listener.last_result().size()); + + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(1.0)}}); + Await(listener, 3); + QuerySnapshot snapshot = listener.last_result(1); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForChange) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForCollectionChange"); + ListenerRegistration registration = + listener.AttachTo(&collection, MetadataChanges::kInclude); + Await(listener); + QuerySnapshot snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(2.0)}}); + Await(listener, 3); + snapshot = listener.last_result(1); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(2.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForDelete) { + CollectionReference collection = + Collection(std::map{ + {"doc", MapFieldValue{{"a", FieldValue::Double(1.0)}}}}); + DocumentReference document = collection.Document("doc"); + TestEventListener listener("TestForQueryDelete"); + ListenerRegistration registration = + listener.AttachTo(&collection, MetadataChanges::kInclude); + Await(listener); + QuerySnapshot snapshot = listener.last_result(); + EXPECT_EQ(1, snapshot.size()); + EXPECT_THAT( + snapshot.documents()[0].GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + DeleteDocument(document); + Await(listener, 2); + snapshot = listener.last_result(); + EXPECT_EQ(0, snapshot.size()); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, + TestMetadataOnlyChangesAreNotFiredWhenNoOptionsProvided) { + DocumentReference document = Collection().Document(); + TestEventListener listener("TestForNoMetadataOnlyChanges"); + ListenerRegistration registration = listener.AttachTo(&document); + WriteDocument(document, MapFieldValue{{"a", FieldValue::Double(1.0)}}); + Await(listener); + EXPECT_THAT( + listener.last_result().GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Double(1.0)}})); + WriteDocument(document, MapFieldValue{{"b", FieldValue::Double(1.0)}}); + Await(listener); + EXPECT_THAT( + listener.last_result().GetData(), + testing::ContainerEq(MapFieldValue{{"b", FieldValue::Double(1.0)}})); + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentReferenceExposesFirestore) { + Firestore* db = firestore(); + // EXPECT_EQ(db, db->Document("foo/bar").firestore()); + // TODO(varconst): use the commented out check above. + // Currently, integration tests create their own Firestore instances that + // aren't registered in the main cache. Because of that, Firestore objects + // will lazily create a new Firestore instance upon the first access. This + // doesn't affect production code, only tests. + // Also, the logic in `util_ios.h` can be modified to make sure that + // `CachedFirestore` doesn't create a new Firestore instance if there isn't + // one already. + EXPECT_NE(nullptr, db->Document("foo/bar").firestore()); +} + +TEST_F(FirestoreIntegrationTest, TestCollectionReferenceExposesFirestore) { + Firestore* db = firestore(); + // EXPECT_EQ(db, db->Collection("foo").firestore()); + EXPECT_NE(nullptr, db->Collection("foo").firestore()); +} + +TEST_F(FirestoreIntegrationTest, TestQueryExposesFirestore) { + Firestore* db = firestore(); + // EXPECT_EQ(db, db->Collection("foo").Limit(5).firestore()); + EXPECT_NE(nullptr, db->Collection("foo").Limit(5).firestore()); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentReferenceEquality) { + Firestore* db = firestore(); + DocumentReference document = db->Document("foo/bar"); + EXPECT_EQ(document, db->Document("foo/bar")); + EXPECT_EQ(document, document.Collection("blah").Parent()); + + EXPECT_NE(document, db->Document("foo/BAR")); + + Firestore* another_db = CachedFirestore("another"); + EXPECT_NE(document, another_db->Document("foo/bar")); +} + +TEST_F(FirestoreIntegrationTest, TestQueryReferenceEquality) { + Firestore* db = firestore(); + Query query = db->Collection("foo").OrderBy("bar").WhereEqualTo( + "baz", FieldValue::Integer(42)); + Query query2 = db->Collection("foo").OrderBy("bar").WhereEqualTo( + "baz", FieldValue::Integer(42)); + EXPECT_EQ(query, query2); + + Query query3 = db->Collection("foo").OrderBy("BAR").WhereEqualTo( + "baz", FieldValue::Integer(42)); + EXPECT_NE(query, query3); + + // PORT_NOTE: Right now there is no way to create another Firestore in test. + // So we skip the testing of two queries with different Firestore instance. +} + +TEST_F(FirestoreIntegrationTest, TestCanTraverseCollectionsAndDocuments) { + Firestore* db = firestore(); + + // doc path from root Firestore. + EXPECT_EQ("a/b/c/d", db->Document("a/b/c/d").path()); + + // collection path from root Firestore. + EXPECT_EQ("a/b/c/d", db->Collection("a/b/c").Document("d").path()); + + // doc path from CollectionReference. + EXPECT_EQ("a/b/c/d", db->Collection("a").Document("b/c/d").path()); + + // collection path from DocumentReference. + EXPECT_EQ("a/b/c/d/e", db->Document("a/b").Collection("c/d/e").path()); +} + +TEST_F(FirestoreIntegrationTest, TestCanTraverseCollectionAndDocumentParents) { + Firestore* db = firestore(); + CollectionReference collection = db->Collection("a/b/c"); + EXPECT_EQ("a/b/c", collection.path()); + + DocumentReference doc = collection.Parent(); + EXPECT_EQ("a/b", doc.path()); + + collection = doc.Parent(); + EXPECT_EQ("a", collection.path()); + + DocumentReference invalidDoc = collection.Parent(); + EXPECT_FALSE(invalidDoc.is_valid()); +} + +TEST_F(FirestoreIntegrationTest, TestCollectionId) { + EXPECT_EQ("foo", firestore()->Collection("foo").id()); + EXPECT_EQ("baz", firestore()->Collection("foo/bar/baz").id()); +} + +TEST_F(FirestoreIntegrationTest, TestDocumentId) { + EXPECT_EQ(firestore()->Document("foo/bar").id(), "bar"); + EXPECT_EQ(firestore()->Document("foo/bar/baz/qux").id(), "qux"); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueueWritesWhileOffline) { + // Arrange + DocumentReference document = Collection("rooms").Document("eros"); + + // Act + Await(firestore()->DisableNetwork()); + Future future = document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}}); + EXPECT_EQ(FutureStatus::kFutureStatusPending, future.status()); + Await(firestore()->EnableNetwork()); + Await(future); + + // Assert + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); +} + +TEST_F(FirestoreIntegrationTest, TestCanGetDocumentsWhileOffline) { + DocumentReference document = Collection("rooms").Document(); + Await(firestore()->DisableNetwork()); + Future future = document.Get(); + Await(future); + EXPECT_EQ(Error::kErrorUnavailable, future.error()); + + // Write the document to the local cache. + Future pending_write = document.Set(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}}); + + // The network is offline and we return a cached result. + DocumentSnapshot snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + EXPECT_TRUE(snapshot.metadata().is_from_cache()); + + // Enable the network and fetch the document again. + Await(firestore()->EnableNetwork()); + Await(pending_write); + snapshot = ReadDocument(document); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"desc", FieldValue::String("Description")}, + {"owner", + FieldValue::Map({{"name", FieldValue::String("Sebastian")}, + {"email", FieldValue::String("abc@xyz.com")}})}})); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); +} + +// Will not port the following two cases: +// TestWriteStreamReconnectsAfterIdle and +// TestWatchStreamReconnectsAfterIdle, +// both of which requires manipulating with DispatchQueue which is not exposed +// as a public API. +// Also, these tests exercise a particular part of SDK (streams), they are +// really unit tests that have to be run in integration tests setup. The +// existing Objective-C and Android tests cover these cases fairly well. + +TEST_F(FirestoreIntegrationTest, TestCanDisableAndEnableNetworking) { + // There's not currently a way to check if networking is in fact disabled, + // so for now just test that the method is well-behaved and doesn't throw. + Firestore* db = firestore(); + Await(db->EnableNetwork()); + Await(db->EnableNetwork()); + Await(db->DisableNetwork()); + Await(db->DisableNetwork()); + Await(db->EnableNetwork()); +} + +// TODO(varconst): split this test. +TEST_F(FirestoreIntegrationTest, TestToString) { + Settings settings; + settings.set_host("foo.bar"); + settings.set_ssl_enabled(false); + EXPECT_EQ( + "Settings(host='foo.bar', is_ssl_enabled=false, " + "is_persistence_enabled=true)", + settings.ToString()); + + CollectionReference collection = Collection("rooms"); + DocumentReference reference = collection.Document("eros"); + // Note: because the map is unordered, it's hard to check the case where a map + // has more than one element. + Await(reference.Set({ + {"owner", FieldValue::String("Jonny")}, + })); + EXPECT_EQ(std::string("DocumentReference(") + collection.id() + "/eros)", + reference.ToString()); + + DocumentSnapshot doc = ReadDocument(reference); + EXPECT_EQ( + "DocumentSnapshot(id=eros, " + "metadata=SnapshotMetadata{has_pending_writes=false, " + "is_from_cache=false}, doc={owner: 'Jonny'})", + doc.ToString()); +} + +// TODO(wuandy): Enable this for other platforms when they can handle +// exceptions. +#if defined(__ANDROID__) +TEST_F(FirestoreIntegrationTest, ClientCallsAfterTerminateFails) { + Await(firestore()->Terminate()); + EXPECT_THROW(Await(firestore()->DisableNetwork()), FirestoreException); +} + +TEST_F(FirestoreIntegrationTest, NewOperationThrowsAfterFirestoreTerminate) { + auto instance = firestore(); + DocumentReference reference = firestore()->Document("abc/123"); + Await(reference.Set({{"Field", FieldValue::Integer(100)}})); + + Await(instance->Terminate()); + + EXPECT_THROW(Await(reference.Get()), FirestoreException); + EXPECT_THROW(Await(reference.Update({{"Field", FieldValue::Integer(1)}})), + FirestoreException); + EXPECT_THROW(Await(reference.Set({{"Field", FieldValue::Integer(1)}})), + FirestoreException); + EXPECT_THROW(Await(instance->batch() + .Set(reference, {{"Field", FieldValue::Integer(1)}}) + .Commit()), + FirestoreException); + EXPECT_THROW(Await(instance->RunTransaction( + [reference](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + transaction.Get(reference, &error, &error_message); + return error; + })), + FirestoreException); +} + +TEST_F(FirestoreIntegrationTest, TerminateCanBeCalledMultipleTimes) { + auto instance = firestore(); + DocumentReference reference = instance->Document("abc/123"); + Await(reference.Set({{"Field", FieldValue::Integer(100)}})); + + Await(instance->Terminate()); + + EXPECT_THROW(Await(reference.Get()), FirestoreException); + + // Calling a second time should go through and change nothing. + Await(instance->Terminate()); + + EXPECT_THROW(Await(reference.Update({{"Field", FieldValue::Integer(1)}})), + FirestoreException); +} +#endif // defined(__ANDROID__) + +TEST_F(FirestoreIntegrationTest, MaintainsPersistenceAfterRestarting) { + DocumentReference doc = firestore()->Collection("col1").Document("doc1"); + auto path = doc.path(); + Await(doc.Set({{"foo", FieldValue::String("bar")}})); + DeleteFirestore(); + + DocumentReference doc_2 = firestore()->Document(path); + auto snap = Await(doc_2.Get()); + EXPECT_TRUE(snap->exists()); +} + +TEST_F(FirestoreIntegrationTest, RestartFirestoreLeadsToNewInstance) { + auto app_name = "non-default-app"; + App* app = GetApp(app_name); + Firestore* db = CreateFirestore(app->name()); + + // Shutdown `db` and create a new instance, make sure they are different + // instances. + Await(db->Terminate()); + auto db_2 = CreateFirestore(app->name()); + EXPECT_NE(db_2, db); + + // Make sure the new instance functions. + Await(db_2->Document("abc/doc").Set({{"foo", FieldValue::String("bar")}})); +} + +TEST_F(FirestoreIntegrationTest, CanStopListeningAfterTerminate) { + auto instance = firestore(); + DocumentReference reference = instance->Document("abc/123"); + EventAccumulator accumulator; + ListenerRegistration registration = + accumulator.listener()->AttachTo(&reference); + + accumulator.Await(); + Await(instance->Terminate()); + + // This should proceed without error. + registration.Remove(); + // Multiple calls should proceed as effectively a no-op. + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, WaitForPendingWritesResolves) { + DocumentReference document = Collection("abc").Document("123"); + + Await(firestore()->DisableNetwork()); + Future await_pending_writes_1 = firestore()->WaitForPendingWrites(); + Future pending_writes = + document.Set(MapFieldValue{{"desc", FieldValue::String("Description")}}); + Future await_pending_writes_2 = firestore()->WaitForPendingWrites(); + + // `await_pending_writes_1` resolves immediately because there are no pending + // writes at the time it is created. + Await(await_pending_writes_1); + EXPECT_EQ(await_pending_writes_1.status(), + FutureStatus::kFutureStatusComplete); + EXPECT_EQ(pending_writes.status(), FutureStatus::kFutureStatusPending); + EXPECT_EQ(await_pending_writes_2.status(), + FutureStatus::kFutureStatusPending); + + firestore()->EnableNetwork(); + Await(await_pending_writes_2); + EXPECT_EQ(await_pending_writes_2.status(), + FutureStatus::kFutureStatusComplete); +} + +// TODO(wuandy): This test requires to create underlying firestore instance with +// a MockCredentialProvider first. +// TEST_F(FirestoreIntegrationTest, WaitForPendingWritesFailsWhenUserChanges) {} + +TEST_F(FirestoreIntegrationTest, + WaitForPendingWritesResolvesWhenOfflineIfThereIsNoPending) { + Await(firestore()->DisableNetwork()); + Future await_pending_writes = firestore()->WaitForPendingWrites(); + + // `await_pending_writes` resolves immediately because there are no pending + // writes at the time it is created. + Await(await_pending_writes); + EXPECT_EQ(await_pending_writes.status(), FutureStatus::kFutureStatusComplete); +} + +TEST_F(FirestoreIntegrationTest, CanClearPersistenceAfterRestarting) { + Firestore* db = CreateFirestore(); + App* app = db->app(); + std::string app_name = app->name(); + + DocumentReference document = db->Collection("a").Document("b"); + std::string path = document.path(); + WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}}); + + // ClearPersistence() requires Firestore to be terminated. Delete the app and + // the Firestore instance to emulate the way an end user would do this. + Await(db->Terminate()); + Await(db->ClearPersistence()); + delete db; + delete app; + + // We restart the app with the same name and options to check that the + // previous instance's persistent storage is actually cleared after the + // restart. Calling firestore() here would create a new instance of firestore, + // which defeats the purpose of this test. + Firestore* db_2 = CreateFirestore(app_name); + DocumentReference document_2 = db_2->Document(path); + Future await_get = document_2.Get(Source::kCache); + Await(await_get); + EXPECT_EQ(await_get.status(), FutureStatus::kFutureStatusComplete); + EXPECT_EQ(await_get.error(), Error::kErrorUnavailable); +} + +TEST_F(FirestoreIntegrationTest, CanClearPersistenceOnANewFirestoreInstance) { + Firestore* db = CreateFirestore(); + App* app = db->app(); + std::string app_name = app->name(); + + DocumentReference document = db->Collection("a").Document("b"); + std::string path = document.path(); + WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}}); + + Await(db->Terminate()); + delete db; + delete app; + + // We restart the app with the same name and options to check that the + // previous instance's persistent storage is actually cleared after the + // restart. Calling firestore() here would create a new instance of firestore, + // which defeats the purpose of this test. + Firestore* db_2 = CreateFirestore(app_name); + Await(db_2->ClearPersistence()); + DocumentReference document_2 = db_2->Document(path); + Future await_get = document_2.Get(Source::kCache); + Await(await_get); + EXPECT_EQ(await_get.status(), FutureStatus::kFutureStatusComplete); + EXPECT_EQ(await_get.error(), Error::kErrorUnavailable); +} + +TEST_F(FirestoreIntegrationTest, ClearPersistenceWhileRunningFails) { + // Call EnableNetwork() in order to ensure that Firestore is fully + // initialized before clearing persistence. EnableNetwork() is chosen because + // it is easy to call. + Await(firestore()->EnableNetwork()); + Future await_clear_persistence = firestore()->ClearPersistence(); + Await(await_clear_persistence); + EXPECT_EQ(await_clear_persistence.status(), + FutureStatus::kFutureStatusComplete); + EXPECT_EQ(await_clear_persistence.error(), Error::kErrorFailedPrecondition); +} + +// Note: this test only exists in C++. +TEST_F(FirestoreIntegrationTest, DomainObjectsReferToSameFirestoreInstance) { + EXPECT_EQ(firestore(), firestore()->Document("foo/bar").firestore()); + EXPECT_EQ(firestore(), firestore()->Collection("foo").firestore()); +} + +#endif // defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/includes_test.cc b/firestore/src/tests/includes_test.cc new file mode 100644 index 0000000000..01b64bde13 --- /dev/null +++ b/firestore/src/tests/includes_test.cc @@ -0,0 +1,87 @@ +#include + +#include "devtools/build/runtime/get_runfiles_dir.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +// This class is a friend of `Firestore`, necessary to access `GetTestInstance`. +class IncludesTest : public testing::Test { + public: + Firestore* CreateFirestore(App* app) { + return new Firestore(CreateTestFirestoreInternal(app)); + } +}; + +namespace { + +struct TestListener : EventListener { + void OnEvent(const int&, Error) override {} +}; + +struct TestTransactionFunction : TransactionFunction { + Error Apply(Transaction&, std::string&) override { return Error::kErrorOk; } +}; + +// This test makes sure that all the objects in Firestore public API are +// available from just including "firestore.h". +// If this test compiles, that is sufficient. +// Not using `FirestoreIntegrationTest` to avoid any headers it includes. +TEST_F(IncludesTest, TestIncludingFirestoreHeaderIsSufficient) { + std::string google_json_dir = devtools_build::testonly::GetTestSrcdir() + + "/google3/firebase/firestore/client/cpp/"; + App::SetDefaultConfigPath(google_json_dir.c_str()); + +#if defined(__ANDROID__) + App* app = App::Create(nullptr, nullptr); + +#elif defined(FIRESTORE_STUB_BUILD) + // Stubs don't load values from `GoogleService-Info.plist`/etc., so the app + // has to be configured explicitly. + AppOptions options; + options.set_project_id("foo"); + options.set_app_id("foo"); + options.set_api_key("foo"); + App* app = App::Create(options); + +#else + App* app = App::Create(); + +#endif // defined(__ANDROID__) + + Firestore* firestore = CreateFirestore(app); + + // Check that Firestore isn't just forward-declared. + DocumentReference doc = firestore->Document("foo/bar"); + Future future = doc.Get(); + DocumentChange doc_change; + DocumentReference doc_ref; + DocumentSnapshot doc_snap; + FieldPath field_path; + FieldValue field_value; + ListenerRegistration listener_registration; + MapFieldValue map_field_value; + MetadataChanges metadata_changes = MetadataChanges::kExclude; + Query query; + QuerySnapshot query_snapshot; + SetOptions set_options; + Settings settings; + SnapshotMetadata snapshot_metadata; + Source source = Source::kDefault; + // Cannot default-construct a `Transaction`. + WriteBatch write_batch; + + TestListener test_listener; + TestTransactionFunction test_transaction_function; + + Timestamp timestamp; + GeoPoint geo_point; + Error error = Error::kErrorOk; +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/listener_registration_test.cc b/firestore/src/tests/listener_registration_test.cc new file mode 100644 index 0000000000..44568d918f --- /dev/null +++ b/firestore/src/tests/listener_registration_test.cc @@ -0,0 +1,185 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#if defined(__ANDROID__) +#include "firestore/src/android/listener_registration_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/listener_registration_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ListenerRegistrationTest.java + +namespace firebase { +namespace firestore { + +using ListenerRegistrationCommonTest = testing::Test; + +class ListenerRegistrationTest : public FirestoreIntegrationTest { + public: + ListenerRegistrationTest() { + firestore()->set_log_level(LogLevel::kLogLevelDebug); + } +}; + +// These tests don't work with stubs. +#if !defined(FIRESTORE_STUB_BUILD) + +TEST_F(ListenerRegistrationTest, TestCanBeRemoved) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + + TestEventListener listener_one("a listener to be removed"); + TestEventListener listener_two("a listener to be removed"); + ListenerRegistration one = listener_one.AttachTo(&collection); + ListenerRegistration two = listener_two.AttachTo(&document); + + // Initial events + Await(listener_one); + Await(listener_two); + EXPECT_EQ(1, listener_one.event_count()); + EXPECT_EQ(1, listener_two.event_count()); + + // Trigger new events + WriteDocument(document, {{"foo", FieldValue::String("bar")}}); + + // Write events should have triggered + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(2, listener_two.event_count()); + + // No more events should occur + one.Remove(); + two.Remove(); + + WriteDocument(document, {{"foo", FieldValue::String("new-bar")}}); + + // Assert no events actually occurred + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(2, listener_two.event_count()); +} + +TEST_F(ListenerRegistrationTest, TestCanBeRemovedTwice) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + + TestEventListener listener_one("a listener to be removed"); + TestEventListener listener_two("a listener to be removed"); + ListenerRegistration one = listener_one.AttachTo(&collection); + ListenerRegistration two = listener_two.AttachTo(&document); + + one.Remove(); + EXPECT_NO_THROW(one.Remove()); + + two.Remove(); + EXPECT_NO_THROW(two.Remove()); +} + +TEST_F(ListenerRegistrationTest, TestCanBeRemovedIndependently) { + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + + TestEventListener listener_one("listener one"); + TestEventListener listener_two("listener two"); + ListenerRegistration one = listener_one.AttachTo(&collection); + ListenerRegistration two = listener_two.AttachTo(&collection); + + // Initial events + Await(listener_one); + Await(listener_two); + + // Triger new events + WriteDocument(document, {{"foo", FieldValue::String("bar")}}); + + // Write events should have triggered + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(2, listener_two.event_count()); + + // Should leave listener number two unaffected + one.Remove(); + + WriteDocument(document, {{"foo", FieldValue::String("new-bar")}}); + + // Assert only events for listener number two actually occurred + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(3, listener_two.event_count()); + + // No more events should occur + two.Remove(); + + // The following check does not exist in the corresponding Android and iOS + // native client SDKs tests. + WriteDocument(document, {{"foo", FieldValue::String("brand-new-bar")}}); + EXPECT_EQ(2, listener_one.event_count()); + EXPECT_EQ(3, listener_two.event_count()); +} + +#endif // defined(FIRESTORE_STUB_BUILD) + +#if defined(__ANDROID__) +// TODO(b/136011600): the mechanism for creating internals doesn't work on iOS. +// The most valuable test is making sure that a copy of a registration can be +// used to remove the listener. + +TEST_F(ListenerRegistrationCommonTest, Construction) { + ListenerRegistrationInternal* internal = + testutil::NewInternal(); + ListenerRegistration registration = FirestoreInternal::Wrap(internal); + EXPECT_EQ(internal, FirestoreInternal::Internal( + registration)); + + ListenerRegistration reg_default; + EXPECT_EQ(nullptr, FirestoreInternal::Internal( + reg_default)); + + ListenerRegistration reg_copy(registration); + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_copy)); + + ListenerRegistration reg_move(std::move(registration)); + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_move)); + + delete internal; +} + +TEST_F(ListenerRegistrationCommonTest, Assignment) { + ListenerRegistrationInternal* internal = + testutil::NewInternal(); + ListenerRegistration registration = FirestoreInternal::Wrap(internal); + ListenerRegistration reg_copy; + reg_copy = registration; + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_copy)); + + ListenerRegistration reg_move; + reg_move = std::move(registration); + EXPECT_EQ(internal, FirestoreInternal::Internal( + reg_move)); + + delete internal; +} + +TEST_F(ListenerRegistrationCommonTest, Remove) { + ListenerRegistrationInternal* internal = + testutil::NewInternal(); + ListenerRegistration registration = FirestoreInternal::Wrap(internal); + ListenerRegistration reg_copy; + reg_copy = registration; + + registration.Remove(); + reg_copy.Remove(); + + delete internal; +} + +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/numeric_transforms_test.cc b/firestore/src/tests/numeric_transforms_test.cc new file mode 100644 index 0000000000..79f8609a0f --- /dev/null +++ b/firestore/src/tests/numeric_transforms_test.cc @@ -0,0 +1,204 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior; + +class NumericTransformsTest : public FirestoreIntegrationTest { + public: + NumericTransformsTest() { + doc_ref_ = Document(); + listener_ = + accumulator_.listener()->AttachTo(&doc_ref_, MetadataChanges::kInclude); + + // Wait for initial null snapshot to avoid potential races. + DocumentSnapshot initial_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_FALSE(initial_snapshot.exists()); + } + + ~NumericTransformsTest() override { listener_.Remove(); } + + protected: + /** Writes values and waits for the corresponding snapshot. */ + void WriteInitialData(const MapFieldValue& doc) { + WriteDocument(doc_ref_, doc); + + accumulator_.AwaitRemoteEvent(); + } + + void ExpectLocalAndRemoteValue(int value) { + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(value, snap.Get("sum").integer_value()); + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(value, snap.Get("sum").integer_value()); + } + + void ExpectLocalAndRemoteValue(double value) { + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(value, snap.Get("sum").double_value()); + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(value, snap.Get("sum").double_value()); + } + + // A document reference to read and write. + DocumentReference doc_ref_; + + // Accumulator used to capture events during the test. + EventAccumulator accumulator_; + + // Listener registration for a listener maintained during the course of the + // test. + ListenerRegistration listener_; +}; + +TEST_F(NumericTransformsTest, CreateDocumentWithIncrement) { + Await(doc_ref_.Set({{"sum", FieldValue::Increment(1337)}})); + + ExpectLocalAndRemoteValue(1337); +} + +TEST_F(NumericTransformsTest, MergeOnNonExistingDocumentWithIncrement) { + MapFieldValue data = {{"sum", FieldValue::Integer(1337)}}; + + Await(doc_ref_.Set(data, SetOptions::Merge())); + + ExpectLocalAndRemoteValue(1337); +} + +TEST_F(NumericTransformsTest, IntegerIncrementWithExistingInteger) { + WriteInitialData({{"sum", FieldValue::Integer(1337)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1)}})); + + ExpectLocalAndRemoteValue(1338); +} + +TEST_F(NumericTransformsTest, DoubleIncrementWithExistingDouble) { + WriteInitialData({{"sum", FieldValue::Double(13.37)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}})); + + ExpectLocalAndRemoteValue(13.47); +} + +TEST_F(NumericTransformsTest, IntegerIncrementWithExistingDouble) { + WriteInitialData({{"sum", FieldValue::Double(13.37)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1)}})); + + ExpectLocalAndRemoteValue(14.37); +} + +TEST_F(NumericTransformsTest, DoubleIncrementWithExistingInteger) { + WriteInitialData({{"sum", FieldValue::Integer(1337)}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}})); + + ExpectLocalAndRemoteValue(1337.1); +} + +TEST_F(NumericTransformsTest, IntegerIncrementWithExistingString) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1337)}})); + + ExpectLocalAndRemoteValue(1337); +} + +TEST_F(NumericTransformsTest, DoubleIncrementWithExistingString) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + Await(doc_ref_.Update({{"sum", FieldValue::Increment(13.37)}})); + + ExpectLocalAndRemoteValue(13.37); +} + +TEST_F(NumericTransformsTest, MultipleDoubleIncrements) { + WriteInitialData({{"sum", FieldValue::Double(0.0)}}); + + DisableNetwork(); + + doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(0.01)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(0.001)}}); + + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(0.1, snap.Get("sum").double_value()); + + snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(0.11, snap.Get("sum").double_value()); + + snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_double()); + EXPECT_DOUBLE_EQ(0.111, snap.Get("sum").double_value()); + + EnableNetwork(); + + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_DOUBLE_EQ(0.111, snap.Get("sum").double_value()); +} + +TEST_F(NumericTransformsTest, IncrementTwiceInABatch) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + WriteBatch batch = firestore()->batch(); + + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(1)}}); + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(1)}}); + + Await(batch.Commit()); + + ExpectLocalAndRemoteValue(2); +} + +TEST_F(NumericTransformsTest, IncrementDeleteIncrementInABatch) { + WriteInitialData({{"sum", FieldValue::String("overwrite")}}); + + WriteBatch batch = firestore()->batch(); + + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(1)}}); + batch.Update(doc_ref_, {{"sum", FieldValue::Delete()}}); + batch.Update(doc_ref_, {{"sum", FieldValue::Increment(3)}}); + + Await(batch.Commit()); + + ExpectLocalAndRemoteValue(3); +} + +TEST_F(NumericTransformsTest, ServerTimestampAndIncrement) { + DisableNetwork(); + + doc_ref_.Set({{"sum", FieldValue::ServerTimestamp()}}); + doc_ref_.Set({{"sum", FieldValue::Increment(1)}}); + + DocumentSnapshot snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + snapshot.Get("sum", ServerTimestampBehavior::kEstimate).is_timestamp()); + + DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(1, snap.Get("sum").integer_value()); + + EnableNetwork(); + + snap = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(snap.Get("sum").is_integer()); + EXPECT_EQ(1, snap.Get("sum").integer_value()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/query_network_test.cc b/firestore/src/tests/query_network_test.cc new file mode 100644 index 0000000000..f8b5627ae5 --- /dev/null +++ b/firestore/src/tests/query_network_test.cc @@ -0,0 +1,148 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/query_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/query_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/QueryTest.java + +namespace firebase { +namespace firestore { + +class QueryNetworkTest : public FirestoreIntegrationTest { + protected: + void TestCanHaveMultipleMutationsWhileOfflineImpl() { + CollectionReference collection = Collection(); + + // set a few docs to known values + WriteDocuments(collection, + {{"doc1", {{"key1", FieldValue::String("value1")}}}, + {"doc2", {{"key2", FieldValue::String("value2")}}}}); + + // go offline for the rest of this test + Await(firestore()->DisableNetwork()); + + // apply *multiple* mutations while offline + collection.Document("doc1").Set({{"key1b", FieldValue::String("value1b")}}); + collection.Document("doc2").Set({{"key2b", FieldValue::String("value2b")}}); + + QuerySnapshot snapshot = ReadDocuments(collection); + EXPECT_TRUE(snapshot.metadata().is_from_cache()); + EXPECT_THAT(QuerySnapshotToValues(snapshot), + testing::ElementsAre( + MapFieldValue{{"key1b", FieldValue::String("value1b")}}, + MapFieldValue{{"key2b", FieldValue::String("value2b")}})); + + Await(firestore()->EnableNetwork()); + } + + void TestWatchSurvivesNetworkDisconnectImpl() { + CollectionReference collection = Collection(); + EventAccumulator accumulator; + accumulator.listener()->set_print_debug_info(true); + ListenerRegistration registration = accumulator.listener()->AttachTo( + &collection, MetadataChanges::kInclude); + EXPECT_TRUE(accumulator.AwaitRemoteEvent().empty()); + + Await(firestore()->DisableNetwork()); + collection.Add(MapFieldValue{{"foo", FieldValue::ServerTimestamp()}}); + Await(firestore()->EnableNetwork()); + + QuerySnapshot snapshot = accumulator.AwaitServerEvent(); + EXPECT_FALSE(snapshot.empty()); + EXPECT_EQ(1, snapshot.size()); + + registration.Remove(); + } + + void TestQueriesFireFromCacheWhenOfflineImpl() { + CollectionReference collection = + Collection({{"a", {{"foo", FieldValue::Integer(1)}}}}); + EventAccumulator accumulator; + accumulator.listener()->set_print_debug_info(true); + ListenerRegistration registration = accumulator.listener()->AttachTo( + &collection, MetadataChanges::kInclude); + + // initial event + QuerySnapshot snapshot = accumulator.AwaitServerEvent(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"foo", FieldValue::Integer(1)}})); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + + // offline event with is_from_cache=true + Await(firestore()->DisableNetwork()); + snapshot = accumulator.Await(); + EXPECT_TRUE(snapshot.metadata().is_from_cache()); + + // back online event with is_from_cache=false + Await(firestore()->EnableNetwork()); + snapshot = accumulator.Await(); + EXPECT_FALSE(snapshot.metadata().is_from_cache()); + registration.Remove(); + } +}; + +#if defined(__ANDROID__) +// Due to how the integration test is set on Android, we cannot make the tests +// that call DisableNetwork/EnableNetwork run in parallel. So we manually make +// them here in a separate test file and run in serial. + +TEST_F(QueryNetworkTest, EnableDisableNetwork) { + std::cout + << "[ RUN ] " + "FirestoreIntegrationTest.TestCanHaveMultipleMutationsWhileOffline" + << std::endl; + TestCanHaveMultipleMutationsWhileOfflineImpl(); + std::cout + << "[ DONE ] " + "FirestoreIntegrationTest.TestCanHaveMultipleMutationsWhileOffline" + << std::endl; + + std::cout + << "[ RUN ] FirestoreIntegrationTest.WatchSurvivesNetworkDisconnect" + << std::endl; + TestWatchSurvivesNetworkDisconnectImpl(); + std::cout + << "[ DONE ] FirestoreIntegrationTest.WatchSurvivesNetworkDisconnect" + << std::endl; + + std::cout << "[ RUN ] " + "FirestoreIntegrationTest.TestQueriesFireFromCacheWhenOffline" + << std::endl; + TestQueriesFireFromCacheWhenOfflineImpl(); + std::cout << "[ DONE ] " + "FirestoreIntegrationTest.TestQueriesFireFromCacheWhenOffline" + << std::endl; +} + +#else + +TEST_F(QueryNetworkTest, TestCanHaveMultipleMutationsWhileOffline) { + TestCanHaveMultipleMutationsWhileOfflineImpl(); +} + +TEST_F(QueryNetworkTest, TestWatchSurvivesNetworkDisconnect) { + TestWatchSurvivesNetworkDisconnectImpl(); +} + +TEST_F(QueryNetworkTest, TestQueriesFireFromCacheWhenOffline) { + TestQueriesFireFromCacheWhenOfflineImpl(); +} + +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/query_snapshot_test.cc b/firestore/src/tests/query_snapshot_test.cc new file mode 100644 index 0000000000..f61e1117df --- /dev/null +++ b/firestore/src/tests/query_snapshot_test.cc @@ -0,0 +1,34 @@ +#include + +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/include/firebase/firestore.h" +#if defined(__ANDROID__) +#include "firestore/src/android/query_snapshot_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/query_snapshot_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +using QuerySnapshotTest = testing::Test; + +TEST_F(QuerySnapshotTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(QuerySnapshotTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/query_test.cc b/firestore/src/tests/query_test.cc new file mode 100644 index 0000000000..a9550c84b0 --- /dev/null +++ b/firestore/src/tests/query_test.cc @@ -0,0 +1,697 @@ +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/query_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/common/wrapper_assertions.h" +#include "firestore/src/stub/query_stub.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/firestore_errors.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/QueryTest.java +// +// Some test cases are moved to query_network_test.cc. Check that file for more +// details. + +namespace firebase { +namespace firestore { + +using QueryTest = testing::Test; + +#if !defined(FIRESTORE_STUB_BUILD) + +TEST_F(FirestoreIntegrationTest, TestLimitQueries) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + QuerySnapshot snapshot = ReadDocuments(collection.Limit(2)); + EXPECT_EQ(std::vector({{{"k", FieldValue::String("a")}}, + {{"k", FieldValue::String("b")}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestLimitQueriesUsingDescendingSortOrder) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Integer(0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Integer(1)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Integer(1)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Integer(2)}}}}); + QuerySnapshot snapshot = ReadDocuments(collection.Limit(2).OrderBy( + FieldPath({"sort"}), Query::Direction::kDescending)); + EXPECT_EQ( + std::vector( + {{{"k", FieldValue::String("d")}, {"sort", FieldValue::Integer(2)}}, + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Integer(1)}}}), + QuerySnapshotToValues(snapshot)); +} + +#if defined(__ANDROID__) +TEST_F(FirestoreIntegrationTest, TestLimitToLastMustAlsoHaveExplicitOrderBy) { + CollectionReference collection = Collection(); + + EXPECT_THROW(Await(collection.LimitToLast(2).Get()), FirestoreException); +} +#endif // defined(__ANDROID__) + +// Two queries that mapped to the same target ID are referred to as "mirror +// queries". An example for a mirror query is a LimitToLast() query and a +// Limit() query that share the same backend Target ID. Since LimitToLast() +// queries are sent to the backend with a modified OrderBy() clause, they can +// map to the same target representation as Limit() query, even if both queries +// appear separate to the user. +TEST_F(FirestoreIntegrationTest, + TestListenUnlistenRelistenSequenceOfMirrorQueries) { + CollectionReference collection = Collection( + {{"a", + {{"k", FieldValue::String("a")}, {"sort", FieldValue::Integer(0)}}}, + {"b", + {{"k", FieldValue::String("b")}, {"sort", FieldValue::Integer(1)}}}, + {"c", + {{"k", FieldValue::String("c")}, {"sort", FieldValue::Integer(1)}}}, + {"d", + {{"k", FieldValue::String("d")}, {"sort", FieldValue::Integer(2)}}}}); + + // Set up `limit` query. + Query limit = + collection.Limit(2).OrderBy("sort", Query::Direction::kAscending); + EventAccumulator limit_accumulator; + ListenerRegistration limit_registration = + limit_accumulator.listener()->AttachTo(&limit); + + // Set up mirroring `limitToLast` query. + Query limit_to_last = + collection.LimitToLast(2).OrderBy("sort", Query::Direction::kDescending); + EventAccumulator limit_to_last_accumulator; + ListenerRegistration limit_to_last_registration = + limit_to_last_accumulator.listener()->AttachTo(&limit_to_last); + + // Verify both queries get expected result. + QuerySnapshot snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}}, + MapFieldValue{{"k", FieldValue::String("b")}, + {"sort", FieldValue::Integer(1)}})); + snapshot = limit_to_last_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("b")}, + {"sort", FieldValue::Integer(1)}}, + MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}})); + + // Unlisten then re-listen to the `limit` query. + limit_registration.Remove(); + limit_registration = limit_accumulator.listener()->AttachTo(&limit); + + // Verify `limit` query still works. + snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}}, + MapFieldValue{{"k", FieldValue::String("b")}, + {"sort", FieldValue::Integer(1)}})); + + // Add a document that would change the result set. + Await(collection.Add(MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}})); + + // Verify both queries get expected result. + snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}}, + MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}})); + snapshot = limit_to_last_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(0)}}, + MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}})); + + // Unlisten to `LimitToLast`, update a doc, then relisten to `LimitToLast` + limit_to_last_registration.Remove(); + Await(collection.Document("a").Update(MapFieldValue{ + {"k", FieldValue::String("a")}, {"sort", FieldValue::Integer(-2)}})); + limit_to_last_registration = + limit_to_last_accumulator.listener()->AttachTo(&limit_to_last); + + // Verify both queries get expected result. + snapshot = limit_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(-2)}}, + MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}})); + snapshot = limit_to_last_accumulator.Await(); + EXPECT_THAT( + QuerySnapshotToValues(snapshot), + testing::ElementsAre(MapFieldValue{{"k", FieldValue::String("e")}, + {"sort", FieldValue::Integer(-1)}}, + MapFieldValue{{"k", FieldValue::String("a")}, + {"sort", FieldValue::Integer(-2)}})); +} + +TEST_F(FirestoreIntegrationTest, + TestKeyOrderIsDescendingForDescendingInequality) { + CollectionReference collection = + Collection({{"a", {{"foo", FieldValue::Integer(42)}}}, + {"b", {{"foo", FieldValue::Double(42.0)}}}, + {"c", {{"foo", FieldValue::Integer(42)}}}, + {"d", {{"foo", FieldValue::Integer(21)}}}, + {"e", {{"foo", FieldValue::Double(21.0)}}}, + {"f", {{"foo", FieldValue::Integer(66)}}}, + {"g", {{"foo", FieldValue::Double(66.0)}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.WhereGreaterThan("foo", FieldValue::Integer(21)) + .OrderBy(FieldPath({"foo"}), Query::Direction::kDescending)); + EXPECT_EQ(std::vector({"g", "f", "c", "b", "a"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestUnaryFilterQueries) { + CollectionReference collection = Collection( + {{"a", {{"null", FieldValue::Null()}, {"nan", FieldValue::Double(NAN)}}}, + {"b", {{"null", FieldValue::Null()}, {"nan", FieldValue::Integer(0)}}}, + {"c", + {{"null", FieldValue::Boolean(false)}, + {"nan", FieldValue::Double(NAN)}}}}); + QuerySnapshot snapshot = + ReadDocuments(collection.WhereEqualTo("null", FieldValue::Null()) + .WhereEqualTo("nan", FieldValue::Double(NAN))); + EXPECT_EQ(std::vector({{{"null", FieldValue::Null()}, + {"nan", FieldValue::Double(NAN)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestQueryWithFieldPaths) { + CollectionReference collection = + Collection({{"a", {{"a", FieldValue::Integer(1)}}}, + {"b", {{"a", FieldValue::Integer(2)}}}, + {"c", {{"a", FieldValue::Integer(3)}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.WhereLessThan(FieldPath({"a"}), FieldValue::Integer(3)) + .OrderBy(FieldPath({"a"}), Query::Direction::kDescending)); + EXPECT_EQ(std::vector({"b", "a"}), QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestFilterOnInfinity) { + CollectionReference collection = + Collection({{"a", {{"inf", FieldValue::Double(INFINITY)}}}, + {"b", {{"inf", FieldValue::Double(-INFINITY)}}}}); + QuerySnapshot snapshot = ReadDocuments( + collection.WhereEqualTo("inf", FieldValue::Double(INFINITY))); + EXPECT_EQ( + std::vector({{{"inf", FieldValue::Double(INFINITY)}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestWillNotGetMetadataOnlyUpdates) { + CollectionReference collection = + Collection({{"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}}); + + TestEventListener listener("no metadata-only update"); + ListenerRegistration registration = listener.AttachTo(&collection); + Await(listener); + EXPECT_EQ(1, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + + WriteDocument(collection.Document("a"), {{"v", FieldValue::String("a1")}}); + EXPECT_EQ(2, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + + registration.Remove(); +} + +TEST_F(FirestoreIntegrationTest, + TestCanListenForTheSameQueryWithDifferentOptions) { + CollectionReference collection = Collection(); + WriteDocuments(collection, {{"a", {{"v", FieldValue::String("a")}}}, + {"b", {{"v", FieldValue::String("b")}}}}); + + // Add two listeners, one tracking metadata-change while the other not. + TestEventListener listener("no metadata-only update"); + TestEventListener listener_full("include metadata update"); + + ListenerRegistration registration_full = + listener_full.AttachTo(&collection, MetadataChanges::kInclude); + ListenerRegistration registration = listener.AttachTo(&collection); + + Await(listener); + Await(listener_full, 2); // Let's make sure both events triggered. + + EXPECT_EQ(1, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + EXPECT_EQ(2, listener_full.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result(1))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result())); + EXPECT_TRUE(listener_full.last_result(1).metadata().is_from_cache()); + EXPECT_FALSE(listener_full.last_result().metadata().is_from_cache()); + + // Change document to trigger the listeners. + WriteDocument(collection.Document("a"), {{"v", FieldValue::String("a1")}}); + // Only one event without options + EXPECT_EQ(2, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener.last_result())); + // Expect two events for the write, once from latency compensation and once + // from the acknowledgement from the server. + Await(listener_full, 4); // Let's make sure both events triggered. + EXPECT_EQ(4, listener_full.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result(1))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b")}}}), + QuerySnapshotToValues(listener_full.last_result())); + EXPECT_TRUE(listener_full.last_result(1).metadata().has_pending_writes()); + EXPECT_FALSE(listener_full.last_result().metadata().has_pending_writes()); + + // Change document again to trigger the listeners. + WriteDocument(collection.Document("b"), {{"v", FieldValue::String("b1")}}); + // Only one event without options + EXPECT_EQ(3, listener.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b1")}}}), + QuerySnapshotToValues(listener.last_result())); + // Expect two events for the write, once from latency compensation and once + // from the acknowledgement from the server. + Await(listener_full, 6); // Let's make sure both events triggered. + EXPECT_EQ(6, listener_full.event_count()); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b1")}}}), + QuerySnapshotToValues(listener_full.last_result(1))); + EXPECT_EQ(std::vector({{{"v", FieldValue::String("a1")}}, + {{"v", FieldValue::String("b1")}}}), + QuerySnapshotToValues(listener_full.last_result())); + EXPECT_TRUE(listener_full.last_result(1).metadata().has_pending_writes()); + EXPECT_FALSE(listener_full.last_result().metadata().has_pending_writes()); + + // Unregister listeners. + registration.Remove(); + registration_full.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestCanListenForQueryMetadataChanges) { + CollectionReference collection = + Collection({{"1", + {{"sort", FieldValue::Double(1.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::Integer(1)}}}, + {"2", + {{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::Integer(2)}}}, + {"3", + {{"sort", FieldValue::Double(3.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::Integer(3)}}}, + {"4", + {{"sort", FieldValue::Double(4.0)}, + {"filter", FieldValue::Boolean(false)}, + {"key", FieldValue::Integer(4)}}}}); + + // The first query does not have any document cached. + TestEventListener listener1("listener to the first query"); + Query collection_with_filter1 = + collection.WhereLessThan("key", FieldValue::Integer(4)); + ListenerRegistration registration1 = + listener1.AttachTo(&collection_with_filter1); + Await(listener1); + EXPECT_EQ(1, listener1.event_count()); + EXPECT_EQ(std::vector({"1", "2", "3"}), + QuerySnapshotToIds(listener1.last_result())); + + // The second query has document cached from the first query. + TestEventListener listener2("listener to the second query"); + Query collection_with_filter2 = + collection.WhereEqualTo("filter", FieldValue::Boolean(true)); + ListenerRegistration registration2 = + listener2.AttachTo(&collection_with_filter2, MetadataChanges::kInclude); + Await(listener2, 2); // Let's make sure both events triggered. + EXPECT_EQ(2, listener2.event_count()); + EXPECT_EQ(std::vector({"1", "2", "3"}), + QuerySnapshotToIds(listener2.last_result(1))); + EXPECT_EQ(std::vector({"1", "2", "3"}), + QuerySnapshotToIds(listener2.last_result())); + EXPECT_TRUE(listener2.last_result(1).metadata().is_from_cache()); + EXPECT_FALSE(listener2.last_result().metadata().is_from_cache()); + + // Unregister listeners. + registration1.Remove(); + registration2.Remove(); +} + +TEST_F(FirestoreIntegrationTest, TestCanExplicitlySortByDocumentId) { + CollectionReference collection = + Collection({{"a", {{"key", FieldValue::String("a")}}}, + {"b", {{"key", FieldValue::String("b")}}}, + {"c", {{"key", FieldValue::String("c")}}}}); + // Ideally this would be descending to validate it's different than + // the default, but that requires an extra index + QuerySnapshot snapshot = + ReadDocuments(collection.OrderBy(FieldPath::DocumentId())); + EXPECT_EQ(std::vector({"a", "b", "c"}), + QuerySnapshotToIds(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueryByDocumentId) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + // Query by Document Id. + QuerySnapshot snapshot1 = ReadDocuments(collection.WhereEqualTo( + FieldPath::DocumentId(), FieldValue::String("ab"))); + EXPECT_EQ(std::vector({"ab"}), QuerySnapshotToIds(snapshot1)); + + // Query by Document Ids. + QuerySnapshot snapshot2 = ReadDocuments( + collection + .WhereGreaterThan(FieldPath::DocumentId(), FieldValue::String("aa")) + .WhereLessThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("ba"))); + EXPECT_EQ(std::vector({"ab", "ba"}), + QuerySnapshotToIds(snapshot2)); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueryByDocumentIdUsingRefs) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + // Query by Document Id. + QuerySnapshot snapshot1 = ReadDocuments(collection.WhereEqualTo( + FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("ab")))); + EXPECT_EQ(std::vector({"ab"}), QuerySnapshotToIds(snapshot1)); + + // Query by Document Ids. + QuerySnapshot snapshot2 = ReadDocuments( + collection + .WhereGreaterThan(FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("aa"))) + .WhereLessThanOrEqualTo( + FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("ba")))); + EXPECT_EQ(std::vector({"ab", "ba"}), + QuerySnapshotToIds(snapshot2)); +} + +TEST_F(FirestoreIntegrationTest, TestCanQueryWithAndWithoutDocumentKey) { + CollectionReference collection = Collection(); + collection.Add({}); + QuerySnapshot snapshot1 = ReadDocuments(collection.OrderBy( + FieldPath::DocumentId(), Query::Direction::kAscending)); + QuerySnapshot snapshot2 = ReadDocuments(collection); + + EXPECT_EQ(QuerySnapshotToValues(snapshot1), QuerySnapshotToValues(snapshot2)); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseArrayContainsFilters) { + CollectionReference collection = Collection( + {{"a", {{"array", FieldValue::Array({FieldValue::Integer(42)})}}}, + {"b", + {{"array", + FieldValue::Array({FieldValue::String("a"), FieldValue::Integer(42), + FieldValue::String("c")})}}}, + {"c", + {{"array", + FieldValue::Array( + {FieldValue::Double(41.999), FieldValue::String("42"), + FieldValue::Map( + {{"a", FieldValue::Array({FieldValue::Integer(42)})}})})}}}, + {"d", + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}}); + // Search for 42 + QuerySnapshot snapshot = ReadDocuments( + collection.WhereArrayContains("array", FieldValue::Integer(42))); + EXPECT_EQ( + std::vector( + {{{"array", FieldValue::Array({FieldValue::Integer(42)})}}, + {{"array", FieldValue::Array({FieldValue::String("a"), + FieldValue::Integer(42), + FieldValue::String("c")})}}, + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}), + QuerySnapshotToValues(snapshot)); + + // NOTE: The backend doesn't currently support null, NaN, objects, or arrays, + // so there isn't much of anything else interesting to test. +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseInFilters) { + CollectionReference collection = Collection( + {{"a", {{"zip", FieldValue::Integer(98101)}}}, + {"b", {{"zip", FieldValue::Integer(98102)}}}, + {"c", {{"zip", FieldValue::Integer(98103)}}}, + {"d", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"e", + {{"zip", + FieldValue::Array( + {FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer(98101)}})})}}}, + {"f", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}}); + // Search for zips matching 98101, 98103, or [98101, 98102]. + QuerySnapshot snapshot = ReadDocuments(collection.WhereIn( + "zip", {FieldValue::Integer(98101), FieldValue::Integer(98103), + FieldValue::Array( + {FieldValue::Integer(98101), FieldValue::Integer(98102)})})); + EXPECT_EQ(std::vector( + {{{"zip", FieldValue::Integer(98101)}}, + {{"zip", FieldValue::Integer(98103)}}, + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}), + QuerySnapshotToValues(snapshot)); + + // With objects. + snapshot = ReadDocuments(collection.WhereIn( + "zip", {FieldValue::Map({{"code", FieldValue::Integer(500)}})})); + EXPECT_EQ( + std::vector( + {{{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseInFiltersWithDocIds) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + QuerySnapshot snapshot = ReadDocuments( + collection.WhereIn(FieldPath::DocumentId(), + {FieldValue::String("aa"), FieldValue::String("ab")})); + EXPECT_EQ(std::vector({{{"key", FieldValue::String("aa")}}, + {{"key", FieldValue::String("ab")}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestQueriesCanUseArrayContainsAnyFilters) { + CollectionReference collection = Collection( + {{"a", {{"array", FieldValue::Array({FieldValue::Integer(42)})}}}, + {"b", + {{"array", + FieldValue::Array({FieldValue::String("a"), FieldValue::Integer(42), + FieldValue::String("c")})}}}, + {"c", + {{"array", + FieldValue::Array( + {FieldValue::Double(41.999), FieldValue::String("42"), + FieldValue::Map( + {{"a", FieldValue::Array({FieldValue::Integer(42)})}})})}}}, + {"d", + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}, + {"e", {{"array", FieldValue::Array({FieldValue::Integer(43)})}}}, + {"f", + {{"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::Integer(42)}})})}}}, + {"g", {{"array", FieldValue::Integer(42)}}}}); + + // Search for "array" to contain [42, 43] + QuerySnapshot snapshot = ReadDocuments(collection.WhereArrayContainsAny( + "array", {FieldValue::Integer(42), FieldValue::Integer(43)})); + EXPECT_EQ(std::vector( + {{{"array", FieldValue::Array({FieldValue::Integer(42)})}}, + {{"array", FieldValue::Array({FieldValue::String("a"), + FieldValue::Integer(42), + FieldValue::String("c")})}}, + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}, + {{"array", FieldValue::Array({FieldValue::Integer(43)})}}}), + QuerySnapshotToValues(snapshot)); + + // With objects + snapshot = ReadDocuments(collection.WhereArrayContainsAny( + "array", {FieldValue::Map({{"a", FieldValue::Integer(42)}})})); + EXPECT_EQ(std::vector( + {{{"array", FieldValue::Array({FieldValue::Map( + {{"a", FieldValue::Integer(42)}})})}}}), + QuerySnapshotToValues(snapshot)); +} + +TEST_F(FirestoreIntegrationTest, TestCollectionGroupQueries) { + Firestore* db = firestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "abc/123/" + collection_group + "/cg-doc1", + "abc/123/" + collection_group + "/cg-doc2", + collection_group + "/cg-doc3", + collection_group + "/cg-doc4", + "def/456/" + collection_group + "/cg-doc5", + collection_group + "/virtual-doc/nested-coll/not-cg-doc", + "x" + collection_group + "/not-cg-doc", + collection_group + "x/not-cg-doc", + "abc/123/" + collection_group + "x/not-cg-doc", + "abc/123/x" + collection_group + "/not-cg-doc", + "abc/" + collection_group, + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + QuerySnapshot query_snapshot = + ReadDocuments(db->CollectionGroup(collection_group)); + EXPECT_EQ(std::vector( + {"cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5"}), + QuerySnapshotToIds(query_snapshot)); +} + +TEST_F(FirestoreIntegrationTest, + TestCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds) { + Firestore* db = firestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "a/a/" + collection_group + "/cg-doc1", + "a/b/a/b/" + collection_group + "/cg-doc2", + "a/b/" + collection_group + "/cg-doc3", + "a/b/c/d/" + collection_group + "/cg-doc4", + "a/c/" + collection_group + "/cg-doc5", + collection_group + "/cg-doc6", + "a/b/nope/nope", + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + QuerySnapshot query_snapshot = + ReadDocuments(db->CollectionGroup(collection_group) + .OrderBy(FieldPath::DocumentId()) + .StartAt({FieldValue::String("a/b")}) + .EndAt({FieldValue::String("a/b0")})); + EXPECT_EQ(std::vector({"cg-doc2", "cg-doc3", "cg-doc4"}), + QuerySnapshotToIds(query_snapshot)); +} + +TEST_F(FirestoreIntegrationTest, + TestCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds) { + Firestore* db = firestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "a/a/" + collection_group + "/cg-doc1", + "a/b/a/b/" + collection_group + "/cg-doc2", + "a/b/" + collection_group + "/cg-doc3", + "a/b/c/d/" + collection_group + "/cg-doc4", + "a/c/" + collection_group + "/cg-doc5", + collection_group + "/cg-doc6", + "a/b/nope/nope", + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + QuerySnapshot query_snapshot = + ReadDocuments(db->CollectionGroup(collection_group) + .WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("a/b")) + .WhereLessThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("a/b0"))); + EXPECT_EQ(std::vector({"cg-doc2", "cg-doc3", "cg-doc4"}), + QuerySnapshotToIds(query_snapshot)); + + query_snapshot = ReadDocuments( + db->CollectionGroup(collection_group) + .WhereGreaterThan(FieldPath::DocumentId(), FieldValue::String("a/b")) + .WhereLessThan( + FieldPath::DocumentId(), + FieldValue::String("a/b/" + collection_group + "/cg-doc3"))); + EXPECT_EQ(std::vector({"cg-doc2"}), + QuerySnapshotToIds(query_snapshot)); +} + +#endif // !defined(FIRESTORE_STUB_BUILD) + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) +TEST_F(QueryTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(QueryTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/sanity_test.cc b/firestore/src/tests/sanity_test.cc new file mode 100644 index 0000000000..0fdc4be14b --- /dev/null +++ b/firestore/src/tests/sanity_test.cc @@ -0,0 +1,38 @@ +// This is a sanity test using gtest. The goal of this test is to make sure the +// way we setup Android C++ test harness actually works. We write test in a +// cross-platform way with gtest and run test with Android JUnit4 test runner +// for Android. We want this sanity test be as simple as possible while using +// the most critical mechanism of gtest. We also print information to stdout +// for debugging if anything goes wrong. + +#include +#include +#include "gtest/gtest.h" + +class SanityTest : public testing::Test { + protected: + void SetUp() override { printf("==== SetUp ====\n"); } + void TearDown() override { printf("==== TearDown ====\n"); } +}; + +// So far, Android native method cannot be inside namespace. So this has to be +// defined outside of any namespace. +TEST_F(SanityTest, TestSanity) { + printf("==== running %s ====\n", __PRETTY_FUNCTION__); + EXPECT_TRUE(true); +} + +TEST_F(SanityTest, TestAnotherSanity) { + printf("==== running %s ====\n", __PRETTY_FUNCTION__); + EXPECT_EQ(1, 1); +} + +// Generally we do not put test inside #if's because Android test harness will +// generate JUnit test whether macro is true or false. It is fine here since the +// test is enabled for Android. +#if __cpp_exceptions +TEST_F(SanityTest, TestThrow) { + printf("==== running %s ====\n", __PRETTY_FUNCTION__); + EXPECT_ANY_THROW({ throw "exception"; }); +} +#endif // __cpp_exceptions diff --git a/firestore/src/tests/server_timestamp_test.cc b/firestore/src/tests/server_timestamp_test.cc new file mode 100644 index 0000000000..1e7880feec --- /dev/null +++ b/firestore/src/tests/server_timestamp_test.cc @@ -0,0 +1,294 @@ +#include +#include +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ServerTimestampTest.java + +namespace firebase { +namespace firestore { + +using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior; + +class ServerTimestampTest : public FirestoreIntegrationTest { + public: + ~ServerTimestampTest() override {} + + protected: + void SetUp() override { + doc_ = Document(); + listener_registration_ = + accumulator_.listener()->AttachTo(&doc_, MetadataChanges::kInclude); + + // Wait for initial null snapshot to avoid potential races. + DocumentSnapshot initial_snapshot = accumulator_.Await(); + EXPECT_FALSE(initial_snapshot.exists()); + } + + void TearDown() override { listener_registration_.Remove(); } + + /** Returns the expected data, with the specified timestamp substituted in. */ + MapFieldValue ExpectedDataWithTimestamp(const FieldValue& timestamp) { + return MapFieldValue{{"a", FieldValue::Integer(42)}, + {"when", timestamp}, + {"deep", FieldValue::Map({{"when", timestamp}})}}; + } + + /** Writes initial_data_ and waits for the corresponding snapshot. */ + void WriteInitialData() { + WriteDocument(doc_, initial_data_); + DocumentSnapshot initial_data_snapshot = accumulator_.Await(); + EXPECT_THAT(initial_data_snapshot.GetData(), + testing::ContainerEq(initial_data_)); + initial_data_snapshot = accumulator_.Await(); + EXPECT_THAT(initial_data_snapshot.GetData(), + testing::ContainerEq(initial_data_)); + } + + /** + * Verifies a snapshot containing set_data_ but with null for the timestamps. + */ + void VerifyTimestampsAreNull(const DocumentSnapshot& snapshot) { + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(ExpectedDataWithTimestamp(FieldValue::Null()))); + } + + /** + * Verifies a snapshot containing set_data_ but with resolved server + * timestamps. + */ + void VerifyTimestampsAreResolved(const DocumentSnapshot& snapshot) { + ASSERT_TRUE(snapshot.exists()); + ASSERT_TRUE(snapshot.Get("when").is_timestamp()); + Timestamp when = snapshot.Get("when").timestamp_value(); + // Tolerate up to 48*60*60 seconds of clock skew between client and server. + // This should be more than enough to compensate for timezone issues (even + // after taking daylight saving into account) and should allow local clocks + // to deviate from true time slightly and still pass the test. PORT_NOTE: + // For the tolerance here, Android uses 48*60*60 seconds while iOS uses 10 + // seconds. + int delta_sec = 48 * 60 * 60; + Timestamp now = Timestamp::Now(); + EXPECT_LT(abs(when.seconds() - now.seconds()), delta_sec) + << "resolved timestamp (" << when.ToString() << ") should be within " + << delta_sec << "s of now (" << now.ToString() << ")"; + + // Validate the rest of the document. + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq( + ExpectedDataWithTimestamp(FieldValue::Timestamp(when)))); + } + + /** + * Verifies a snapshot containing set_data_ but with local estimates for + * server timestamps. + */ + void VerifyTimestampsAreEstimates(const DocumentSnapshot& snapshot) { + ASSERT_TRUE(snapshot.exists()); + FieldValue when = snapshot.Get("when", ServerTimestampBehavior::kEstimate); + ASSERT_TRUE(when.is_timestamp()); + EXPECT_THAT(snapshot.GetData(ServerTimestampBehavior::kEstimate), + testing::ContainerEq(ExpectedDataWithTimestamp(when))); + } + + /** + * Verifies a snapshot containing set_data_ but using the previous field value + * for server timestamps. + */ + void VerifyTimestampsUsePreviousValue(const DocumentSnapshot& snapshot, + const FieldValue& previous) { + ASSERT_TRUE(snapshot.exists()); + ASSERT_TRUE(previous.is_null() || previous.is_timestamp()); + EXPECT_THAT(snapshot.GetData(ServerTimestampBehavior::kPrevious), + testing::ContainerEq(ExpectedDataWithTimestamp(previous))); + } + + // Data written in tests via set. + const MapFieldValue set_data_ = MapFieldValue{ + {"a", FieldValue::Integer(42)}, + {"when", FieldValue::ServerTimestamp()}, + {"deep", FieldValue::Map({{"when", FieldValue::ServerTimestamp()}})}}; + + // Base and update data used for update tests. + const MapFieldValue initial_data_ = + MapFieldValue{{"a", FieldValue::Integer(42)}}; + const MapFieldValue update_data_ = MapFieldValue{ + {"when", FieldValue::ServerTimestamp()}, + {"deep", FieldValue::Map({{"when", FieldValue::ServerTimestamp()}})}}; + + // A document reference to read and write to. + DocumentReference doc_; + + // Accumulator used to capture events during the test. + EventAccumulator accumulator_; + + // Listener registration for a listener maintained during the course of the + // test. + ListenerRegistration listener_registration_; +}; + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaSet) { + WriteDocument(doc_, set_data_); + VerifyTimestampsAreNull(accumulator_.AwaitLocalEvent()); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaUpdate) { + WriteInitialData(); + UpdateDocument(doc_, update_data_); + VerifyTimestampsAreNull(accumulator_.AwaitLocalEvent()); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsCanReturnEstimatedValue) { + WriteDocument(doc_, set_data_); + VerifyTimestampsAreEstimates(accumulator_.AwaitLocalEvent()); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsCanReturnPreviousValue) { + WriteDocument(doc_, set_data_); + VerifyTimestampsUsePreviousValue(accumulator_.AwaitLocalEvent(), + FieldValue::Null()); + DocumentSnapshot previous_snapshot = accumulator_.AwaitRemoteEvent(); + VerifyTimestampsAreResolved(previous_snapshot); + + UpdateDocument(doc_, update_data_); + VerifyTimestampsUsePreviousValue(accumulator_.AwaitLocalEvent(), + previous_snapshot.Get("when")); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsCanReturnPreviousValueOfDifferentType) { + WriteInitialData(); + UpdateDocument(doc_, MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + + DocumentSnapshot local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE(local_snapshot.Get("a").is_null()); + EXPECT_TRUE(local_snapshot.Get("a", ServerTimestampBehavior::kEstimate) + .is_timestamp()); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + DocumentSnapshot remote_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(remote_snapshot.Get("a").is_timestamp()); + EXPECT_TRUE(remote_snapshot.Get("a", ServerTimestampBehavior::kEstimate) + .is_timestamp()); + EXPECT_TRUE(remote_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .is_timestamp()); +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsCanRetainPreviousValueThroughConsecutiveUpdates) { + WriteInitialData(); + Await(firestore()->DisableNetwork()); + accumulator_.AwaitRemoteEvent(); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + DocumentSnapshot local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + Await(firestore()->EnableNetwork()); + + DocumentSnapshot remote_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(remote_snapshot.Get("a").is_timestamp()); +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsUsesPreviousValueFromLocalMutation) { + WriteInitialData(); + Await(firestore()->DisableNetwork()); + accumulator_.AwaitRemoteEvent(); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + DocumentSnapshot local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(42, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + doc_.Update(MapFieldValue{{"a", FieldValue::Integer(1337)}}); + accumulator_.AwaitLocalEvent(); + + doc_.Update(MapFieldValue{{"a", FieldValue::ServerTimestamp()}}); + local_snapshot = accumulator_.AwaitLocalEvent(); + EXPECT_TRUE( + local_snapshot.Get("a", ServerTimestampBehavior::kPrevious).is_integer()); + EXPECT_EQ(1337, local_snapshot.Get("a", ServerTimestampBehavior::kPrevious) + .integer_value()); + + Await(firestore()->EnableNetwork()); + + DocumentSnapshot remote_snapshot = accumulator_.AwaitRemoteEvent(); + EXPECT_TRUE(remote_snapshot.Get("a").is_timestamp()); +} + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaTransactionSet) { +#if defined(FIREBASE_USE_STD_FUNCTION) + Await(firestore()->RunTransaction( + [this](Transaction& transaction, std::string&) -> Error { + transaction.Set(doc_, set_data_); + return Error::kErrorOk; + })); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(ServerTimestampTest, TestServerTimestampsWorkViaTransactionUpdate) { +#if defined(FIREBASE_USE_STD_FUNCTION) + WriteInitialData(); + Await(firestore()->RunTransaction( + [this](Transaction& transaction, std::string&) -> Error { + transaction.Update(doc_, update_data_); + return Error::kErrorOk; + })); + VerifyTimestampsAreResolved(accumulator_.AwaitRemoteEvent()); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsFailViaTransactionUpdateOnNonexistentDocument) { +#if defined(FIREBASE_USE_STD_FUNCTION) + Future future = firestore()->RunTransaction( + [this](Transaction& transaction, std::string&) -> Error { + transaction.Update(doc_, update_data_); + return Error::kErrorOk; + }); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +TEST_F(ServerTimestampTest, + TestServerTimestampsFailViaUpdateOnNonexistentDocument) { + Future future = doc_.Update(update_data_); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/smoke_test.cc b/firestore/src/tests/smoke_test.cc new file mode 100644 index 0000000000..6466a08e32 --- /dev/null +++ b/firestore/src/tests/smoke_test.cc @@ -0,0 +1,165 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FSTSmokeTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/SmokeTest.java + +namespace firebase { +namespace firestore { + +using TypeTest = FirestoreIntegrationTest; + +TEST_F(TypeTest, TestCanWriteASingleDocument) { + const MapFieldValue test_data{ + {"name", FieldValue::String("Patryk")}, + {"message", FieldValue::String("We are actually writing data!")}}; + CollectionReference collection = Collection(); + Await(collection.Add(test_data)); +} + +TEST_F(TypeTest, TestCanReadAWrittenDocument) { + const MapFieldValue test_data{{"foo", FieldValue::String("bar")}}; + CollectionReference collection = Collection(); + + DocumentReference new_reference = *Await(collection.Add(test_data)); + DocumentSnapshot result = *Await(new_reference.Get()); + EXPECT_THAT( + result.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +} + +TEST_F(TypeTest, TestObservesExistingDocument) { + const MapFieldValue test_data{{"foo", FieldValue::String("bar")}}; + DocumentReference writer_reference = + CachedFirestore("writer")->Collection("collection").Document(); + DocumentReference reader_reference = CachedFirestore("reader") + ->Collection("collection") + .Document(writer_reference.id()); + Await(writer_reference.Set(test_data)); + + EventAccumulator accumulator; + ListenerRegistration registration = accumulator.listener()->AttachTo( + &reader_reference, MetadataChanges::kInclude); + + DocumentSnapshot doc = accumulator.Await(); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); + registration.Remove(); +} + +TEST_F(TypeTest, TestObservesNewDocument) { + CollectionReference collection = Collection(); + DocumentReference writer_reference = collection.Document(); + DocumentReference reader_reference = + collection.Document(writer_reference.id()); + + EventAccumulator accumulator; + ListenerRegistration registration = accumulator.listener()->AttachTo( + &reader_reference, MetadataChanges::kInclude); + + DocumentSnapshot doc = accumulator.Await(); + EXPECT_FALSE(doc.exists()); + + const MapFieldValue test_data{{"foo", FieldValue::String("bar")}}; + Await(writer_reference.Set(test_data)); + + doc = accumulator.Await(); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); + EXPECT_TRUE(doc.metadata().has_pending_writes()); + + doc = accumulator.Await(); + EXPECT_THAT( + doc.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); + EXPECT_FALSE(doc.metadata().has_pending_writes()); + + registration.Remove(); +} + +TEST_F(TypeTest, TestWillFireValueEventsForEmptyCollections) { + CollectionReference collection = Collection(); + EventAccumulator accumulator; + ListenerRegistration registration = + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + + QuerySnapshot query_snapshot = accumulator.Await(); + EXPECT_EQ(0, query_snapshot.size()); + EXPECT_TRUE(query_snapshot.empty()); + + registration.Remove(); +} + +TEST_F(TypeTest, TestGetCollectionQuery) { + const std::map test_data{ + {"1", + {{"name", FieldValue::String("Patryk")}, + {"message", FieldValue::String("Real data, yo!")}}}, + {"2", + {{"name", FieldValue::String("Gil")}, + {"message", FieldValue::String("Yep!")}}}, + {"3", + {{"name", FieldValue::String("Jonny")}, + {"message", FieldValue::String("Back to work!")}}}}; + CollectionReference collection = Collection(test_data); + QuerySnapshot result = *Await(collection.Get()); + EXPECT_FALSE(result.empty()); + EXPECT_THAT( + QuerySnapshotToValues(result), + testing::ElementsAre( + MapFieldValue{{"name", FieldValue::String("Patryk")}, + {"message", FieldValue::String("Real data, yo!")}}, + MapFieldValue{{"name", FieldValue::String("Gil")}, + {"message", FieldValue::String("Yep!")}}, + MapFieldValue{{"name", FieldValue::String("Jonny")}, + {"message", FieldValue::String("Back to work!")}})); +} + +// TODO(klimt): This test is disabled because we can't create compound indexes +// programmatically. +TEST_F(TypeTest, DISABLED_TestQueryByFieldAndUseOrderBy) { + const std::map test_data{ + {"1", + {{"sort", FieldValue::Double(1.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("1")}}}, + {"2", + {{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("2")}}}, + {"3", + {{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("3")}}}, + {"4", + {{"sort", FieldValue::Double(3.0)}, + {"filter", FieldValue::Boolean(false)}, + {"key", FieldValue::String("4")}}}}; + CollectionReference collection = Collection(test_data); + Query query = collection.WhereEqualTo("filter", FieldValue::Boolean(true)) + .OrderBy("sort", Query::Direction::kDescending); + QuerySnapshot result = *Await(query.Get()); + EXPECT_THAT( + QuerySnapshotToValues(result), + testing::ElementsAre(MapFieldValue{{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("2")}}, + MapFieldValue{{"sort", FieldValue::Double(2.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("3")}}, + MapFieldValue{{"sort", FieldValue::Double(1.0)}, + {"filter", FieldValue::Boolean(true)}, + {"key", FieldValue::String("1")}})); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/transaction_extra_test.cc b/firestore/src/tests/transaction_extra_test.cc new file mode 100644 index 0000000000..fad6361b17 --- /dev/null +++ b/firestore/src/tests/transaction_extra_test.cc @@ -0,0 +1,114 @@ +#include "app/src/time.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/firestore_errors.h" +#if defined(__ANDROID__) +#include "firestore/src/android/transaction_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/transaction_stub.h" +#endif // defined(__ANDROID__) + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FSTTransactionTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/TransactionTest.java + +namespace firebase { +namespace firestore { + +// We will be using lambda in the test instead of defining a +// TransactionFunction for each of the test case. +// +// We do have a TransactionFunction-version of the test +// TestGetNonexistentDocumentThenCreate to test the non-lambda API. + +using TransactionExtraTest = FirestoreIntegrationTest; + +#if defined(FIREBASE_USE_STD_FUNCTION) + +TEST_F(TransactionExtraTest, + TestRetriesWhenDocumentThatWasReadWithoutBeingWrittenChanges) { + DocumentReference doc1 = firestore()->Collection("counter").Document(); + DocumentReference doc2 = firestore()->Collection("counter").Document(); + WriteDocument(doc1, MapFieldValue{{"count", FieldValue::Integer(15)}}); + // Use these two as a portable way to mimic atomic integer. + Mutex mutex; + int transaction_runs_count = 0; + + Future future = firestore()->RunTransaction([&doc1, &doc2, &mutex, + &transaction_runs_count]( + Transaction& + transaction, + std::string& + error_message) + -> Error { + { + MutexLock lock(mutex); + ++transaction_runs_count; + } + // Get the first doc. + Error error = Error::kErrorOk; + DocumentSnapshot snapshot1 = transaction.Get(doc1, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + // Do a write outside of the transaction. The first time the + // transaction is tried, this will bump the version, which + // will cause the write to doc2 to fail. The second time, it + // will be a no-op and not bump the version. + // Now try to update the other doc from within the transaction. + Await(doc1.Set(MapFieldValue{{"count", FieldValue::Integer(1234)}})); + // Now try to update the other doc from within the transaction. + // This should fail once, because we read 15 earlier. + transaction.Set(doc2, MapFieldValue{{"count", FieldValue::Integer(16)}}); + return Error::kErrorOk; + }); + Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()); + EXPECT_EQ(2, transaction_runs_count); + DocumentSnapshot snapshot = ReadDocument(doc1); + EXPECT_EQ(1234, snapshot.Get("count").integer_value()); +} + +TEST_F(TransactionExtraTest, TestReadingADocTwiceWithDifferentVersions) { + int counter = 0; + DocumentReference doc = firestore()->Collection("counters").Document(); + WriteDocument(doc, MapFieldValue{{"count", FieldValue::Double(15.0)}}); + + Future future = firestore()->RunTransaction( + [&doc, &counter](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + // Get the doc once. + DocumentSnapshot snapshot1 = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + // Do a write outside of the transaction. Because the transaction will + // retry, set the document to a different value each time. + Await(doc.Set( + MapFieldValue{{"count", FieldValue::Double(1234.0 + counter)}})); + ++counter; + // Get the doc again in the transaction with the new version. + DocumentSnapshot snapshot2 = + transaction.Get(doc, &error, &error_message); + // We cannot check snapshot2, which is invalid as the second read would + // have already failed. + + // Now try to update the doc from within the transaction. + // This should fail, because we read 15 earlier. + transaction.Set(doc, + MapFieldValue{{"count", FieldValue::Double(16.0)}}); + return error; + }); + Await(future); + EXPECT_EQ(Error::kErrorAborted, future.error()); + EXPECT_STREQ("Document version changed between two reads.", + future.error_message()); + + DocumentSnapshot snapshot = ReadDocument(doc); +} + +#endif // defined(FIREBASE_USE_STD_FUNCTION) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/transaction_test.cc b/firestore/src/tests/transaction_test.cc new file mode 100644 index 0000000000..09cb0d70ca --- /dev/null +++ b/firestore/src/tests/transaction_test.cc @@ -0,0 +1,750 @@ +#include +#include + +#if !defined(FIRESTORE_STUB_BUILD) +#include "app/src/mutex.h" +#include "app/src/semaphore.h" +#include "app/src/time.h" +#endif + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "absl/strings/str_join.h" +#include "firebase/firestore/firestore_errors.h" +#if defined(__ANDROID__) +#include "firestore/src/android/transaction_android.h" +#elif defined(FIRESTORE_STUB_BUILD) +#include "firestore/src/stub/transaction_stub.h" + +#endif // defined(__ANDROID__) + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FSTTransactionTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/TransactionTest.java +// +// Some test cases are moved to transaction_extra_test.cc. If run together, the +// test will run too long and timeout. + +namespace firebase { +namespace firestore { + +// These tests don't work with the stubs. +#if !defined(FIRESTORE_STUB_BUILD) + +using ::testing::HasSubstr; + +// We will be using lambda in the test instead of defining a +// TransactionFunction for each of the test case. +// +// We do have a TransactionFunction-version of the test +// TestGetNonexistentDocumentThenCreate to test the non-lambda API. + +class TransactionTest : public FirestoreIntegrationTest { + protected: +#if defined(FIREBASE_USE_STD_FUNCTION) + // We occasionally get transient error like "Could not reach Cloud Firestore + // backend. Backend didn't respond within 10 seconds". Transaction requires + // online and thus will not retry. So we do the retry in the testcase. + void RunTransactionAndExpect( + Error error, const char* message, + std::function update) { + Future future; + // Re-try 5 times in case server is unavailable. + for (int i = 0; i < 5; ++i) { + future = firestore()->RunTransaction(update); + Await(future); + if (future.error() == Error::kErrorUnavailable) { + std::cout << "Could not reach backend. Retrying transaction test." + << std::endl; + } else { + break; + } + } + EXPECT_EQ(error, future.error()); + EXPECT_THAT(future.error_message(), HasSubstr(message)); + } + + void RunTransactionAndExpect( + Error error, std::function update) { + switch (error) { + case Error::kErrorOk: + RunTransactionAndExpect(Error::kErrorOk, "", std::move(update)); + break; + case Error::kErrorAborted: + RunTransactionAndExpect( +#if defined(__APPLE__) + Error::kErrorFailedPrecondition, +#else + Error::kErrorAborted, +#endif + "Transaction failed all retries.", std::move(update)); + break; + case Error::kErrorFailedPrecondition: + // Here specifies error message of the most common cause. There are + // other causes for FailedPrecondition as well. Use the one with message + // parameter if the expected error message is different. + RunTransactionAndExpect(Error::kErrorFailedPrecondition, + "Can't update a document that doesn't exist.", + std::move(update)); + break; + default: + FAIL() << "Unexpected error code: " << error; + } + } +#endif // defined(FIREBASE_USE_STD_FUNCTION) +}; + +class TestTransactionFunction : public TransactionFunction { + public: + TestTransactionFunction(DocumentReference doc) : doc_(doc) {} + + Error Apply(Transaction& transaction, std::string& error_message) override { + Error error = Error::kErrorUnknown; + DocumentSnapshot snapshot = transaction.Get(doc_, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + EXPECT_FALSE(snapshot.exists()); + transaction.Set(doc_, MapFieldValue{{key_, FieldValue::String(value_)}}); + return error; + } + + std::string key() { return key_; } + std::string value() { return value_; } + + private: + DocumentReference doc_; + const std::string key_{"foo"}; + const std::string value_{"bar"}; +}; + +TEST_F(TransactionTest, TestGetNonexistentDocumentThenCreatePortableVersion) { + DocumentReference doc = firestore()->Collection("towns").Document(); + TestTransactionFunction transaction{doc}; + Future future = firestore()->RunTransaction(&transaction); + Await(future); + + EXPECT_EQ(Error::kErrorOk, future.error()); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_EQ(FieldValue::String(transaction.value()), + snapshot.Get(transaction.key())); +} + +#if defined(FIREBASE_USE_STD_FUNCTION) + +class TransactionStage { + public: + TransactionStage( + std::string tag, + std::function func) + : tag_(std::move(tag)), func_(std::move(func)) {} + + const std::string& tag() const { return tag_; } + + void operator()(Transaction* transaction, + const DocumentReference& doc) const { + func_(transaction, doc); + } + + bool operator==(const TransactionStage& rhs) const { + return tag_ == rhs.tag_; + } + + bool operator!=(const TransactionStage& rhs) const { + return tag_ != rhs.tag_; + } + + private: + std::string tag_; + std::function func_; +}; + +/** + * The transaction stages that follow are postfixed by numbers to indicate the + * calling order. For example, calling `set1` followed by `set2` should result + * in the document being set to the value specified by `set2`. + */ +const auto delete1 = new TransactionStage( + "delete", [](Transaction* transaction, const DocumentReference& doc) { + transaction->Delete(doc); + }); + +const auto update1 = new TransactionStage("update", [](Transaction* transaction, + const DocumentReference& + doc) { + transaction->Update(doc, MapFieldValue{{"foo", FieldValue::String("bar1")}}); +}); + +const auto update2 = new TransactionStage("update", [](Transaction* transaction, + const DocumentReference& + doc) { + transaction->Update(doc, MapFieldValue{{"foo", FieldValue::String("bar2")}}); +}); + +const auto set1 = new TransactionStage( + "set", [](Transaction* transaction, const DocumentReference& doc) { + transaction->Set(doc, MapFieldValue{{"foo", FieldValue::String("bar1")}}); + }); + +const auto set2 = new TransactionStage( + "set", [](Transaction* transaction, const DocumentReference& doc) { + transaction->Set(doc, MapFieldValue{{"foo", FieldValue::String("bar2")}}); + }); + +const auto get = new TransactionStage( + "get", [](Transaction* transaction, const DocumentReference& doc) { + Error error; + std::string msg; + transaction->Get(doc, &error, &msg); + }); + +/** + * Used for testing that all possible combinations of executing transactions + * result in the desired document value or error. + * + * `Run()`, `WithExistingDoc()`, and `WithNonexistentDoc()` don't actually do + * anything except assign variables into the `TransactionTester`. + * + * `ExpectDoc()`, `ExpectNoDoc()`, and `ExpectError()` will trigger the + * transaction to run and assert that the end result matches the input. + */ +class TransactionTester { + public: + explicit TransactionTester(Firestore* db) : db_(db) {} + + template + TransactionTester& Run(Args... args) { + stages_ = {*args...}; + return *this; + } + + TransactionTester& WithExistingDoc() { + from_existing_doc_ = true; + return *this; + } + + TransactionTester& WithNonexistentDoc() { + from_existing_doc_ = false; + return *this; + } + + void ExpectDoc(const MapFieldValue& expected) { + PrepareDoc(); + RunSuccessfulTransaction(); + Future future = doc_.Get(); + const DocumentSnapshot* snapshot = FirestoreIntegrationTest::Await(future); + EXPECT_TRUE(snapshot->exists()); + EXPECT_THAT(snapshot->GetData(), expected); + stages_.clear(); + } + + void ExpectNoDoc() { + PrepareDoc(); + RunSuccessfulTransaction(); + Future future = doc_.Get(); + const DocumentSnapshot* snapshot = FirestoreIntegrationTest::Await(future); + EXPECT_FALSE(snapshot->exists()); + stages_.clear(); + } + + void ExpectError(Error error) { + PrepareDoc(); + RunFailingTransaction(error); + stages_.clear(); + } + + private: + void PrepareDoc() { + doc_ = db_->Collection("tx-tester").Document(); + if (from_existing_doc_) { + FirestoreIntegrationTest::Await( + doc_.Set(MapFieldValue{{"foo", FieldValue::String("bar0")}})); + } + } + + void RunSuccessfulTransaction() { + Future future = db_->RunTransaction( + [this](Transaction& transaction, std::string& error_message) { + for (const auto& stage : stages_) { + stage(&transaction, doc_); + } + return Error::kErrorOk; + }); + FirestoreIntegrationTest::Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()) + << "Expected the sequence (" + ListStages() + ") to succeed, but got " + + std::to_string(future.error()); + } + + void RunFailingTransaction(Error error) { + Future future = db_->RunTransaction( + [this](Transaction& transaction, std::string& error_message) { + for (const auto& stage : stages_) { + stage(&transaction, doc_); + } + return Error::kErrorOk; + }); + FirestoreIntegrationTest::Await(future); + EXPECT_EQ(error, future.error()) + << "Expected the sequence (" + ListStages() + + ") to fail with the error " + std::to_string(error); + } + + std::string ListStages() const { + std::vector stages; + for (const auto& stage : stages_) { + stages.push_back(stage.tag()); + } + return absl::StrJoin(stages, ","); + } + + Firestore* db_ = nullptr; + DocumentReference doc_; + bool from_existing_doc_ = false; + std::vector stages_; +}; + +TEST_F(TransactionTest, TestRunsTransactionsAfterGettingNonexistentDoc) { + SCOPED_TRACE("TestRunsTransactionsAfterGettingNonexistentDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithExistingDoc().Run(get, delete1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(get, delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithExistingDoc() + .Run(get, delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(get, update1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(get, update1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(get, update1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(get, set1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(get, set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(get, set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestRunsTransactionsAfterGettingExistingDoc) { + SCOPED_TRACE("TestRunsTransactionsAfterGettingExistingDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithNonexistentDoc().Run(get, delete1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(get, delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(get, delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithNonexistentDoc() + .Run(get, update1, delete1) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(get, update1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(get, update1, set2) + .ExpectError(Error::kErrorInvalidArgument); + + tt.WithNonexistentDoc().Run(get, set1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(get, set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithNonexistentDoc() + .Run(get, set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestRunsTransactionsOnExistingDoc) { + SCOPED_TRACE("TestRunTransactionsOnExistingDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithExistingDoc().Run(delete1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithExistingDoc() + .Run(get, delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(update1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(update1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(update1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithExistingDoc().Run(set1, delete1).ExpectNoDoc(); + tt.WithExistingDoc() + .Run(set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithExistingDoc() + .Run(set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestRunsTransactionsOnNonexistentDoc) { + SCOPED_TRACE("TestRunsTransactionsOnNonexistentDoc"); + + TransactionTester tt = TransactionTester(firestore()); + tt.WithNonexistentDoc().Run(delete1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(delete1, update2) + .ExpectError(Error::kErrorInvalidArgument); + tt.WithNonexistentDoc() + .Run(delete1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + + tt.WithNonexistentDoc() + .Run(update1, delete1) + .ExpectError(Error::kErrorNotFound); + tt.WithNonexistentDoc() + .Run(update1, update2) + .ExpectError(Error::kErrorNotFound); + tt.WithNonexistentDoc().Run(update1, set2).ExpectError(Error::kErrorNotFound); + + tt.WithNonexistentDoc().Run(set1, delete1).ExpectNoDoc(); + tt.WithNonexistentDoc() + .Run(set1, update2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); + tt.WithNonexistentDoc() + .Run(set1, set2) + .ExpectDoc(MapFieldValue{{"foo", FieldValue::String("bar2")}}); +} + +TEST_F(TransactionTest, TestGetNonexistentDocumentThenFailPatch) { + DocumentReference doc = firestore()->Collection("towns").Document(); + + SCOPED_TRACE("TestGetNonexistentDocumentThenFailPatch"); + RunTransactionAndExpect( + Error::kErrorInvalidArgument, + "Can't update a document that doesn't exist.", + [doc](Transaction& transaction, std::string& error_message) -> Error { + Error error = Error::kErrorOk; + DocumentSnapshot snapshot = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + EXPECT_FALSE(snapshot.exists()); + transaction.Update(doc, + MapFieldValue{{"foo", FieldValue::String("bar")}}); + return error; + }); +} + +TEST_F(TransactionTest, TestSetDocumentWithMerge) { + DocumentReference doc = firestore()->Collection("towns").Document(); + + SCOPED_TRACE("TestSetDocumentWithMerge"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Set( + doc, + MapFieldValue{{"a", FieldValue::String("b")}, + {"nested", FieldValue::Map(MapFieldValue{ + {"a", FieldValue::String("b")}})}}); + transaction.Set( + doc, + MapFieldValue{{"c", FieldValue::String("d")}, + {"nested", FieldValue::Map(MapFieldValue{ + {"c", FieldValue::String("d")}})}}, + SetOptions::Merge()); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("b")}, + {"c", FieldValue::String("d")}, + {"nested", FieldValue::Map(MapFieldValue{ + {"a", FieldValue::String("b")}, + {"c", FieldValue::String("d")}})}})); +} + +TEST_F(TransactionTest, TestCannotUpdateNonExistentDocument) { + DocumentReference doc = firestore()->Collection("towns").Document(); + + SCOPED_TRACE("TestCannotUpdateNonExistentDocument"); + RunTransactionAndExpect( + Error::kErrorNotFound, "", + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Update(doc, + MapFieldValue{{"foo", FieldValue::String("bar")}}); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(TransactionTest, TestIncrementTransactionally) { + // A set of concurrent transactions. + std::vector> transaction_tasks; + // A barrier to make sure every transaction reaches the same spot. + Semaphore write_barrier{0}; + // Use these two as a portable way to mimic atomic integer. + Mutex started_locker; + int started = 0; + + DocumentReference doc = firestore()->Collection("counters").Document(); + WriteDocument(doc, MapFieldValue{{"count", FieldValue::Double(5.0)}}); + + // Make 3 transactions that will all increment. + // Note: Visual Studio 2015 incorrectly requires `kTotal` to be captured in + // the lambda, even though it's a constant expression. Adding `static` as + // a workaround. + static constexpr int kTotal = 3; + for (int i = 0; i < kTotal; ++i) { + transaction_tasks.push_back(firestore()->RunTransaction( + [doc, &write_barrier, &started_locker, &started]( + Transaction& transaction, std::string& error_message) -> Error { + Error error = Error::kErrorOk; + DocumentSnapshot snapshot = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + { + MutexLock lock(started_locker); + ++started; + // Once all of the transactions have read, allow the first write. + if (started == kTotal) { + write_barrier.Post(); + } + } + + // Let all of the transactions fetch the old value and stop once. + write_barrier.Wait(); + // Refill the barrier so that the other transactions and retries + // succeed. + write_barrier.Post(); + + double new_count = snapshot.Get("count").double_value() + 1.0; + transaction.Set( + doc, MapFieldValue{{"count", FieldValue::Double(new_count)}}); + return error; + })); + } + + // Until we have another Await() that waits for multiple Futures, we do the + // wait in one by one. + while (!transaction_tasks.empty()) { + Future future = transaction_tasks.back(); + transaction_tasks.pop_back(); + Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()); + } + // Now all transaction should be completed, so check the result. + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_DOUBLE_EQ(5.0 + kTotal, snapshot.Get("count").double_value()); +} + +TEST_F(TransactionTest, TestUpdateTransactionally) { + // A set of concurrent transactions. + std::vector> transaction_tasks; + // A barrier to make sure every transaction reaches the same spot. + Semaphore write_barrier{0}; + // Use these two as a portable way to mimic atomic integer. + Mutex started_locker; + int started = 0; + + DocumentReference doc = firestore()->Collection("counters").Document(); + WriteDocument(doc, MapFieldValue{{"count", FieldValue::Double(5.0)}, + {"other", FieldValue::String("yes")}}); + + // Make 3 transactions that will all increment. + static const constexpr int kTotal = 3; + for (int i = 0; i < kTotal; ++i) { + transaction_tasks.push_back(firestore()->RunTransaction( + [doc, &write_barrier, &started_locker, &started]( + Transaction& transaction, std::string& error_message) -> Error { + Error error = Error::kErrorOk; + DocumentSnapshot snapshot = + transaction.Get(doc, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + { + MutexLock lock(started_locker); + ++started; + // Once all of the transactions have read, allow the first write. + if (started == kTotal) { + write_barrier.Post(); + } + } + + // Let all of the transactions fetch the old value and stop once. + write_barrier.Wait(); + // Refill the barrier so that the other transactions and retries + // succeed. + write_barrier.Post(); + + double new_count = snapshot.Get("count").double_value() + 1.0; + transaction.Update( + doc, MapFieldValue{{"count", FieldValue::Double(new_count)}}); + return error; + })); + } + + // Until we have another Await() that waits for multiple Futures, we do the + // wait in backward order. + while (!transaction_tasks.empty()) { + Future future = transaction_tasks.back(); + transaction_tasks.pop_back(); + Await(future); + EXPECT_EQ(Error::kErrorOk, future.error()); + } + // Now all transaction should be completed, so check the result. + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_DOUBLE_EQ(5.0 + kTotal, snapshot.Get("count").double_value()); + EXPECT_EQ("yes", snapshot.Get("other").string_value()); +} + +TEST_F(TransactionTest, TestUpdateFieldsWithDotsTransactionally) { + DocumentReference doc = firestore()->Collection("fieldnames").Document(); + WriteDocument(doc, MapFieldValue{{"a.b", FieldValue::String("old")}, + {"c.d", FieldValue::String("old")}, + {"e.f", FieldValue::String("old")}}); + + SCOPED_TRACE("TestUpdateFieldsWithDotsTransactionally"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Update(doc, MapFieldPathValue{{FieldPath{"a.b"}, + FieldValue::String("new")}}); + transaction.Update(doc, MapFieldPathValue{{FieldPath{"c.d"}, + FieldValue::String("new")}}); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a.b", FieldValue::String("new")}, + {"c.d", FieldValue::String("new")}, + {"e.f", FieldValue::String("old")}})); +} + +TEST_F(TransactionTest, TestUpdateNestedFieldsTransactionally) { + DocumentReference doc = firestore()->Collection("fieldnames").Document(); + WriteDocument( + doc, MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("old")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("old")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}}); + + SCOPED_TRACE("TestUpdateNestedFieldsTransactionally"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc](Transaction& transaction, std::string& error_message) -> Error { + transaction.Update(doc, + MapFieldValue{{"a.b", FieldValue::String("new")}}); + transaction.Update(doc, + MapFieldValue{{"c.d", FieldValue::String("new")}}); + return Error::kErrorOk; + }); + + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("new")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("new")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); +} + +#if defined(__ANDROID__) +// TODO(b/136012313): on iOS, this triggers assertion failure. +TEST_F(TransactionTest, TestCannotReadAfterWriting) { + DocumentReference doc = firestore()->Collection("anything").Document(); + DocumentSnapshot snapshot; + + SCOPED_TRACE("TestCannotReadAfterWriting"); + RunTransactionAndExpect( + Error::kErrorInvalidArgument, + "Firestore transactions require all reads to be " + "executed before all writes.", + [doc, &snapshot](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + transaction.Set(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + snapshot = transaction.Get(doc, &error, &error_message); + return error; + }); + + snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} +#endif + +TEST_F(TransactionTest, TestCanHaveGetsWithoutMutations) { + DocumentReference doc1 = firestore()->Collection("foo").Document(); + DocumentReference doc2 = firestore()->Collection("foo").Document(); + WriteDocument(doc1, MapFieldValue{{"foo", FieldValue::String("bar")}}); + DocumentSnapshot snapshot; + + SCOPED_TRACE("TestCanHaveGetsWithoutMutations"); + RunTransactionAndExpect( + Error::kErrorOk, + [doc1, doc2, &snapshot](Transaction& transaction, + std::string& error_message) -> Error { + Error error = Error::kErrorOk; + transaction.Get(doc2, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + snapshot = transaction.Get(doc1, &error, &error_message); + EXPECT_EQ(Error::kErrorOk, error); + return error; + }); + EXPECT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +} + +TEST_F(TransactionTest, TestSuccessWithNoTransactionOperations) { + SCOPED_TRACE("TestSuccessWithNoTransactionOperations"); + RunTransactionAndExpect( + Error::kErrorOk, + [](Transaction&, std::string&) -> Error { return Error::kErrorOk; }); +} + +TEST_F(TransactionTest, TestCancellationOnError) { + DocumentReference doc = firestore()->Collection("towns").Document(); + // Use these two as a portable way to mimic atomic integer. + Mutex count_locker; + int count = 0; + + SCOPED_TRACE("TestCancellationOnError"); + RunTransactionAndExpect( + Error::kErrorDeadlineExceeded, "no", + [doc, &count_locker, &count](Transaction& transaction, + std::string& error_message) -> Error { + { + MutexLock lock{count_locker}; + ++count; + } + transaction.Set(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + error_message = "no"; + return Error::kErrorDeadlineExceeded; + }); + + // TODO(varconst): uncomment. Currently, there is no way in C++ to distinguish + // user error, so the transaction gets retried, and the counter goes up to 6. + // EXPECT_EQ(1, count); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +#endif // defined(FIREBASE_USE_STD_FUNCTION) + +#endif // defined(__ANDROID__) || defined(__APPLE__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/type_test.cc b/firestore/src/tests/type_test.cc new file mode 100644 index 0000000000..40e005039f --- /dev/null +++ b/firestore/src/tests/type_test.cc @@ -0,0 +1,71 @@ +#include "app/src/log.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRTypeTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/TypeTest.java + +namespace firebase { +namespace firestore { + +class TypeTest : public FirestoreIntegrationTest { + public: + // Write the specified data to Firestore as a document and read that document. + // Check the data read from that document matches with the original data. + void AssertSuccessfulRoundTrip(MapFieldValue data) { + firestore()->set_log_level(LogLevel::kLogLevelDebug); + DocumentReference reference = firestore()->Document("rooms/eros"); + WriteDocument(reference, data); + DocumentSnapshot snapshot = ReadDocument(reference); + EXPECT_TRUE(snapshot.exists()); + EXPECT_EQ(snapshot.GetData(), data); + } +}; + +TEST_F(TypeTest, TestCanReadAndWriteNullFields) { + AssertSuccessfulRoundTrip( + {{"a", FieldValue::Integer(1)}, {"b", FieldValue::Null()}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteArrayFields) { + AssertSuccessfulRoundTrip( + {{"array", FieldValue::Array( + {FieldValue::Integer(1), FieldValue::String("foo"), + FieldValue::Map({{"deep", FieldValue::Boolean(true)}}), + FieldValue::Null()})}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteBlobFields) { + uint8_t blob[3] = {0, 1, 2}; + AssertSuccessfulRoundTrip({{"blob", FieldValue::Blob(blob, 3)}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteGeoPointFields) { + AssertSuccessfulRoundTrip({{"geoPoint", FieldValue::GeoPoint({1.23, 4.56})}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteDateFields) { + AssertSuccessfulRoundTrip( + {{"date", FieldValue::Timestamp(Timestamp::FromTimeT(1491847082))}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteTimestampFields) { + AssertSuccessfulRoundTrip( + {{"date", FieldValue::Timestamp({123456, 123456000})}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteDocumentReferences) { + AssertSuccessfulRoundTrip({{"a", FieldValue::Integer(42)}, + {"ref", FieldValue::Reference(Document())}}); +} + +TEST_F(TypeTest, TestCanReadAndWriteDocumentReferencesInArrays) { + AssertSuccessfulRoundTrip( + {{"a", FieldValue::Integer(42)}, + {"refs", FieldValue::Array({FieldValue::Reference(Document())})}}); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/util/integration_test_util.cc b/firestore/src/tests/util/integration_test_util.cc new file mode 100644 index 0000000000..75039eff8f --- /dev/null +++ b/firestore/src/tests/util/integration_test_util.cc @@ -0,0 +1,66 @@ +#include // NOLINT(build/c++11) +#include // NOLINT(build/c++11) + +#include "devtools/build/runtime/get_runfiles_dir.h" +#include "app/src/include/firebase/app.h" +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/ios/firestore_ios.h" +#include "firestore/src/ios/hard_assert_ios.h" +#include "absl/memory/memory.h" +#include "Firestore/core/src/auth/empty_credentials_provider.h" + +namespace firebase { +namespace firestore { + +using auth::EmptyCredentialsProvider; + +struct TestFriend { + static FirestoreInternal* CreateTestFirestoreInternal(App* app) { + return new FirestoreInternal(app, + absl::make_unique()); + } +}; + +App* GetApp(const char* name) { + // TODO(varconst): try to avoid using a real project ID when possible. iOS + // unit tests achieve this by using fake options: + // https://github.com/firebase/firebase-ios-sdk/blob/9a5afbffc17bb63b7bb7f51b9ea9a6a9e1c88a94/Firestore/core/test/firebase/firestore/testutil/app_testing.mm#L29 + + // Note: setting the default config path doesn't affect anything on iOS. + // This is done unconditionally to simplify the logic. + std::string google_json_dir = devtools_build::testonly::GetTestSrcdir() + + "/google3/firebase/firestore/client/cpp/"; + App::SetDefaultConfigPath(google_json_dir.c_str()); + + if (name == nullptr || std::string{name} == kDefaultAppName) { + return App::Create(); + } else { + App* default_app = App::GetInstance(); + HARD_ASSERT_IOS(default_app, + "Cannot create a named app before the default app"); + return App::Create(default_app->options(), name); + } +} + +App* GetApp() { return GetApp(nullptr); } + +// TODO(varconst): it's brittle and potentially flaky, look into using some +// notification mechanism instead. +bool ProcessEvents(int millis) { + std::this_thread::sleep_for(std::chrono::milliseconds(millis)); + // `false` means "don't shut down the application". + return false; +} + +FirestoreInternal* CreateTestFirestoreInternal(App* app) { + return TestFriend::CreateTestFirestoreInternal(app); +} + +#ifndef __APPLE__ +void InitializeFirestore(Firestore* instance) { + Firestore::set_log_level(LogLevel::kLogLevelDebug); +} +#endif // #ifndef __APPLE__ + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/util/integration_test_util_apple.mm b/firestore/src/tests/util/integration_test_util_apple.mm new file mode 100644 index 0000000000..5125f264c8 --- /dev/null +++ b/firestore/src/tests/util/integration_test_util_apple.mm @@ -0,0 +1,21 @@ +#include + +#include +#include + +#include "firestore/src/include/firebase/firestore.h" + +namespace firebase { +namespace firestore { + +// Note: currently, this file has to be Objective-C++ (`.mm`), because `Settings` are defined in +// such a way that configuring the dispatch queue is only possible within Objective-C++ translation +// units. +// TODO(varconst): fix this somehow. + +void InitializeFirestore(Firestore* instance) { + Firestore::set_log_level(LogLevel::kLogLevelDebug); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/validation_test.cc b/firestore/src/tests/validation_test.cc new file mode 100644 index 0000000000..2cb243a194 --- /dev/null +++ b/firestore/src/tests/validation_test.cc @@ -0,0 +1,885 @@ +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include "firestore/src/android/util_android.h" +#endif // defined(__ANDROID__) +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" +#include "firebase/firestore/firestore_errors.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRValidationTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/ValidationTest.java +// +// PORT_NOTE: C++ API Guidelines (http://g3doc/firebase/g3doc/cpp-api-style.md) +// discourage the use of exceptions in the Firebase Games's SDK. So in release, +// we do not throw exception while only dump exception info to logs. However, in +// order to test this behavior, we enable exception here and check exceptions. + +namespace firebase { +namespace firestore { + +// This eventually works for iOS as well and becomes the cross-platform test for +// C++ client SDK. For now, only enabled for Android platform. + +#if defined(__ANDROID__) + +class ValidationTest : public FirestoreIntegrationTest { + protected: + /** + * Performs a write using each write API and makes sure it fails with the + * expected reason. + */ + void ExpectWriteError(const MapFieldValue& data, const std::string& reason) { + ExpectWriteError(data, reason, /*include_sets=*/true, + /*include_updates=*/true); + } + + /** + * Performs a write using each update API and makes sure it fails with the + * expected reason. + */ + void ExpectUpdateError(const MapFieldValue& data, const std::string& reason) { + ExpectWriteError(data, reason, /*include_sets=*/false, + /*include_updates=*/true); + } + + /** + * Performs a write using each set API and makes sure it fails with the + * expected reason. + */ + void ExpectSetError(const MapFieldValue& data, const std::string& reason) { + ExpectWriteError(data, reason, /*include_sets=*/true, + /*include_updates=*/false); + } + + /** + * Performs a write using each set and/or update API and makes sure it fails + * with the expected reason. + */ + void ExpectWriteError(const MapFieldValue& data, const std::string& reason, + bool include_sets, bool include_updates) { + DocumentReference document = Document(); + + if (include_sets) { + try { + document.Set(data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + firestore()->batch().Set(document, data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } + + if (include_updates) { + try { + document.Update(data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + firestore()->batch().Update(document, data); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } + +#if defined(FIREBASE_USE_STD_FUNCTION) + Await(firestore()->RunTransaction( + [data, reason, include_sets, include_updates, document]( + Transaction& transaction, std::string& error_message) -> Error { + if (include_sets) { + transaction.Set(document, data); + } + if (include_updates) { + transaction.Update(document, data); + } + return Error::kErrorOk; + })); +#endif // defined(FIREBASE_USE_STD_FUNCTION) + } + + /** + * Tests a field path with all of our APIs that accept field paths and ensures + * they fail with the specified reason. + */ + // TODO(varconst): this function is pretty much commented out. + void VerifyFieldPathThrows(const std::string& path, + const std::string& reason) { + // Get an arbitrary snapshot we can use for testing. + DocumentReference document = Document(); + WriteDocument(document, MapFieldValue{{"test", FieldValue::Integer(1)}}); + DocumentSnapshot snapshot = ReadDocument(document); + + // snapshot paths + try { + // TODO(varconst): The logic is in the C++ core and is a hard assertion. + // snapshot.Get(path); + // FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + + // Query filter / order fields + CollectionReference collection = Collection(); + // WhereLessThan(), etc. omitted for brevity since the code path is + // trivially shared. + try { + // TODO(zxu): The logic is in the C++ core and is a hard assertion. + // collection.WhereEqualTo(path, FieldValue::Integer(1)); + // FAIL() << "should throw exception" << path; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + // TODO(zxu): The logic is in the C++ core and is a hard assertion. + // collection.OrderBy(path); + // FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + + // update() paths. + try { + document.Update(MapFieldValue{{path, FieldValue::Integer(1)}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } +}; + +// PORT_NOTE: Does not apply to C++ as host parameter is passed by value. +TEST_F(ValidationTest, FirestoreSettingsNullHostFails) {} + +TEST_F(ValidationTest, ChangingSettingsAfterUseFails) { + DocumentReference reference = Document(); + // Force initialization of the underlying client + WriteDocument(reference, MapFieldValue{{"key", FieldValue::String("value")}}); + Settings setting; + setting.set_host("foo"); + try { + firestore()->set_settings(setting); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "FirebaseFirestore has already been started and its settings can no " + "longer be changed. You can only call setFirestoreSettings() before " + "calling any other methods on a FirebaseFirestore object.", + exception.what()); + } +} + +TEST_F(ValidationTest, DisableSslWithoutSettingHostFails) { + Settings setting; + setting.set_ssl_enabled(false); + try { + firestore()->set_settings(setting); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "You can't set the 'sslEnabled' setting unless you also set a " + "non-default 'host'.", + exception.what()); + } +} + +// PORT_NOTE: Does not apply to C++ as host parameter is passed by value. +TEST_F(ValidationTest, FirestoreGetInstanceWithNullAppFails) {} + +TEST_F(ValidationTest, + FirestoreGetInstanceWithNonNullAppReturnsNonNullInstance) { + try { + InitResult result; + Firestore::GetInstance(app(), &result); + EXPECT_EQ(kInitResultSuccess, result); + } catch (const FirestoreException& exception) { + FAIL() << "shouldn't throw exception"; + } +} + +TEST_F(ValidationTest, CollectionPathsMustBeOddLength) { + Firestore* db = firestore(); + DocumentReference base_document = db->Document("foo/bar"); + std::vector bad_absolute_paths = {"foo/bar", "foo/bar/baz/quu"}; + std::vector bad_relative_paths = {"/", "baz/quu"}; + std::vector expect_errors = { + "Invalid collection reference. Collection references must have an odd " + "number of segments, but foo/bar has 2", + "Invalid collection reference. Collection references must have an odd " + "number of segments, but foo/bar/baz/quu has 4", + }; + for (int i = 0; i < expect_errors.size(); ++i) { + try { + db->Collection(bad_absolute_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + try { + base_document.Collection(bad_relative_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + } +} + +TEST_F(ValidationTest, PathsMustNotHaveEmptySegments) { + Firestore* db = firestore(); + // NOTE: leading / trailing slashes are okay. + db->Collection("/foo/"); + db->Collection("/foo"); + db->Collection("foo/"); + + std::vector bad_paths = {"foo//bar//baz", "//foo", "foo//"}; + CollectionReference collection = db->Collection("test-collection"); + DocumentReference document = collection.Document("test-document"); + for (const std::string& path : bad_paths) { + std::string reason = + "Invalid path (" + path + "). Paths must not contain // in them."; + try { + db->Collection(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + db->Document(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + collection.Document(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + try { + document.Collection(path); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(reason, exception.what()); + } + } +} + +TEST_F(ValidationTest, DocumentPathsMustBeEvenLength) { + Firestore* db = firestore(); + CollectionReference base_collection = db->Collection("foo"); + std::vector bad_absolute_paths = {"foo", "foo/bar/baz"}; + std::vector bad_relative_paths = {"/", "bar/baz"}; + std::vector expect_errors = { + "Invalid document reference. Document references must have an even " + "number of segments, but foo has 1", + "Invalid document reference. Document references must have an even " + "number of segments, but foo/bar/baz has 3", + }; + for (int i = 0; i < expect_errors.size(); ++i) { + try { + db->Document(bad_absolute_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + try { + base_collection.Document(bad_relative_paths[i]); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_EQ(expect_errors[i], exception.what()); + } + } +} + +// PORT_NOTE: Does not apply to C++ which is strong-typed. +TEST_F(ValidationTest, WritesMustBeMapsOrPOJOs) {} + +TEST_F(ValidationTest, WritesMustNotContainDirectlyNestedLists) { + SCOPED_TRACE("WritesMustNotContainDirectlyNestedLists"); + + ExpectWriteError( + MapFieldValue{ + {"nested-array", + FieldValue::Array({FieldValue::Integer(1), + FieldValue::Array({FieldValue::Integer(2)})})}}, + "Invalid data. Nested arrays are not supported"); +} + +TEST_F(ValidationTest, WritesMayContainIndirectlyNestedLists) { + MapFieldValue data = { + {"nested-array", + FieldValue::Array( + {FieldValue::Integer(1), + FieldValue::Map({{"foo", FieldValue::Integer(2)}})})}}; + + CollectionReference collection = Collection(); + DocumentReference document = collection.Document(); + DocumentReference another_document = collection.Document(); + + Await(document.Set(data)); + Await(firestore()->batch().Set(document, data).Commit()); + + Await(document.Update(data)); + Await(firestore()->batch().Update(document, data).Commit()); + +#if defined(FIREBASE_USE_STD_FUNCTION) + Await(firestore()->RunTransaction( + [data, document, another_document](Transaction& transaction, + std::string& error_message) -> Error { + // Note another_document does not exist at this point so set that and + // update document. + transaction.Update(document, data); + transaction.Set(another_document, data); + return Error::kErrorOk; + })); +#endif // defined(FIREBASE_USE_STD_FUNCTION) +} + +// TODO(zxu): There is no way to create Firestore with different project id yet. +TEST_F(ValidationTest, WritesMustNotContainReferencesToADifferentDatabase) {} + +TEST_F(ValidationTest, WritesMustNotContainReservedFieldNames) { + SCOPED_TRACE("WritesMustNotContainReservedFieldNames"); + + ExpectWriteError(MapFieldValue{{"__baz__", FieldValue::Integer(1)}}, + "Invalid data. Document fields cannot begin and end with " + "\"__\" (found in field __baz__)"); + ExpectWriteError( + MapFieldValue{ + {"foo", FieldValue::Map({{"__baz__", FieldValue::Integer(1)}})}}, + "Invalid data. Document fields cannot begin and end with \"__\" (found " + "in " + "field foo.__baz__)"); + ExpectWriteError( + MapFieldValue{ + {"__baz__", FieldValue::Map({{"foo", FieldValue::Integer(1)}})}}, + "Invalid data. Document fields cannot begin and end with \"__\" (found " + "in " + "field __baz__)"); + + ExpectUpdateError(MapFieldValue{{"__baz__", FieldValue::Integer(1)}}, + "Invalid data. Document fields cannot begin and end with " + "\"__\" (found in field __baz__)"); + ExpectUpdateError(MapFieldValue{{"baz.__foo__", FieldValue::Integer(1)}}, + "Invalid data. Document fields cannot begin and end with " + "\"__\" (found in field baz.__foo__)"); +} + +TEST_F(ValidationTest, SetsMustNotContainFieldValueDelete) { + SCOPED_TRACE("SetsMustNotContainFieldValueDelete"); + + ExpectSetError( + MapFieldValue{{"foo", FieldValue::Delete()}}, + "Invalid data. FieldValue.delete() can only be used with update() and " + "set() with SetOptions.merge() (found in field foo)"); +} + +TEST_F(ValidationTest, UpdatesMustNotContainNestedFieldValueDeletes) { + SCOPED_TRACE("UpdatesMustNotContainNestedFieldValueDeletes"); + + ExpectUpdateError( + MapFieldValue{{"foo", FieldValue::Map({{"bar", FieldValue::Delete()}})}}, + "Invalid data. FieldValue.delete() can only appear at the top level of " + "your update data (found in field foo.bar)"); +} + +TEST_F(ValidationTest, BatchWritesRequireCorrectDocumentReferences) { + DocumentReference bad_document = + CachedFirestore("another")->Document("foo/bar"); + + WriteBatch batch = firestore()->batch(); + try { + batch.Set(bad_document, MapFieldValue{{"foo", FieldValue::Integer(1)}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Provided document reference is from a different Cloud Firestore " + "instance.", + exception.what()); + } +} + +TEST_F(ValidationTest, TransactionsRequireCorrectDocumentReferences) {} + +TEST_F(ValidationTest, FieldPathsMustNotHaveEmptySegments) { + SCOPED_TRACE("FieldPathsMustNotHaveEmptySegments"); + + std::map bad_field_paths_and_errors = { + {"", + "Invalid field path (). Paths must not be empty, begin with '.', end " + "with '.', or contain '..'"}, + {"foo..baz", + "Invalid field path (foo..baz). Paths must not be empty, begin with " + "'.', end with '.', or contain '..'"}, + {".foo", + "Invalid field path (.foo). Paths must not be empty, begin with '.', " + "end with '.', or contain '..'"}, + {"foo.", + "Invalid field path (foo.). Paths must not be empty, begin with '.', " + "end with '.', or contain '..'"}}; + for (const auto path_and_error : bad_field_paths_and_errors) { + VerifyFieldPathThrows(path_and_error.first, path_and_error.second); + } +} + +TEST_F(ValidationTest, FieldPathsMustNotHaveInvalidSegments) { + SCOPED_TRACE("FieldPathsMustNotHaveInvalidSegments"); + + std::map bad_field_paths_and_errors = { + {"foo~bar", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo*bar", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo/bar", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo[1", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo]1", "Use FieldPath.of() for field names containing '~*/[]'."}, + {"foo[1]", "Use FieldPath.of() for field names containing '~*/[]'."}, + }; + for (const auto path_and_error : bad_field_paths_and_errors) { + VerifyFieldPathThrows(path_and_error.first, path_and_error.second); + } +} + +TEST_F(ValidationTest, FieldNamesMustNotBeEmpty) { + DocumentSnapshot snapshot = ReadDocument(Document()); + // PORT_NOTE: We do not enforce any logic for invalid C++ object. In + // particular the creation of invalid object should be valid (for using + // standard container). We have not defined the behavior to call API with + // invalid object yet. + // try { + // snapshot.Get(FieldPath{}); + // FAIL() << "should throw exception"; + // } catch (const FirestoreException& exception) { + // EXPECT_STREQ("Invalid field path. Provided path must not be empty.", + // exception.what()); + // } + + try { + snapshot.Get(FieldPath{""}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid field name at argument 1. Field names must not be null or " + "empty.", + exception.what()); + } + try { + snapshot.Get(FieldPath{"foo", ""}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid field name at argument 2. Field names must not be null or " + "empty.", + exception.what()); + } +} + +TEST_F(ValidationTest, ArrayTransformsFailInQueries) { + CollectionReference collection = Collection(); + try { + collection.WhereEqualTo( + "test", + FieldValue::Map( + {{"test", FieldValue::ArrayUnion({FieldValue::Integer(1)})}})); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid data. FieldValue.arrayUnion() can only be used with set() and " + "update() (found in field test)", + exception.what()); + } + + try { + collection.WhereEqualTo( + "test", + FieldValue::Map( + {{"test", FieldValue::ArrayRemove({FieldValue::Integer(1)})}})); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid data. FieldValue.arrayRemove() can only be used with set() " + "and update() (found in field test)", + exception.what()); + } +} + +// PORT_NOTE: Does not apply to C++ which is strong-typed. +TEST_F(ValidationTest, ArrayTransformsRejectInvalidElements) {} + +TEST_F(ValidationTest, ArrayTransformsRejectArrays) { + DocumentReference document = Document(); + // This would result in a directly nested array which is not supported. + try { + document.Set(MapFieldValue{ + {"x", FieldValue::ArrayUnion( + {FieldValue::Integer(1), + FieldValue::Array({FieldValue::String("nested")})})}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ("Invalid data. Nested arrays are not supported", + exception.what()); + } + try { + document.Set(MapFieldValue{ + {"x", FieldValue::ArrayRemove( + {FieldValue::Integer(1), + FieldValue::Array({FieldValue::String("nested")})})}}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ("Invalid data. Nested arrays are not supported", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithNonPositiveLimitFail) { + CollectionReference collection = Collection(); + try { + collection.Limit(0); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Query limit (0) is invalid. Limit must be positive.", + exception.what()); + } + try { + collection.Limit(-1); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Query limit (-1) is invalid. Limit must be positive.", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithNullOrNaNFiltersOtherThanEqualityFail) { + CollectionReference collection = Collection(); + try { + collection.WhereGreaterThan("a", FieldValue::Null()); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Null supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } + try { + collection.WhereArrayContains("a", FieldValue::Null()); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. Null supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } + try { + collection.WhereGreaterThan("a", FieldValue::Double(NAN)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. NaN supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } + try { + collection.WhereArrayContains("a", FieldValue::Double(NAN)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. NaN supports only equality comparisons (via " + "whereEqualTo()).", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesCannotBeCreatedFromDocumentsMissingSortValues) { + CollectionReference collection = + Collection(std::map{ + {"f", MapFieldValue{{"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}}}}); + + Query query = collection.OrderBy("sort"); + DocumentSnapshot snapshot = ReadDocument(collection.Document("f")); + + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"k", FieldValue::String("f")}, + {"nosort", FieldValue::Double(1.0)}})); + const char* reason = + "Invalid query. You are trying to start or end a query using a document " + "for which the field 'sort' (used as the orderBy) does not exist."; + try { + query.StartAt(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.StartAfter(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.EndBefore(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.EndAt(snapshot); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } +} + +TEST_F(ValidationTest, + QueriesCannotBeSortedByAnUncommittedServerTimestamp) { + CollectionReference collection = Collection(); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection); + + Await(firestore()->DisableNetwork()); + + Future future = collection.Document("doc").Set( + {{"timestamp", FieldValue::ServerTimestamp()}}); + + QuerySnapshot snapshot = accumulator.Await(); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + + snapshot = accumulator.Await(); + EXPECT_TRUE(snapshot.metadata().has_pending_writes()); + + EXPECT_THROW(collection.OrderBy(FieldPath({"timestamp"})) + .EndAt(snapshot.documents().at(0)) + .AddSnapshotListener([](const QuerySnapshot&, Error) {}), + FirestoreException); + + Await(firestore()->EnableNetwork()); + Await(future); + + snapshot = accumulator.Await(); + EXPECT_FALSE(snapshot.metadata().has_pending_writes()); + EXPECT_NO_THROW(collection.OrderBy(FieldPath({"timestamp"})) + .EndAt(snapshot.documents().at(0)) + .AddSnapshotListener([](const QuerySnapshot&, Error) {})); +} + + +TEST_F(ValidationTest, QueriesMustNotHaveMoreComponentsThanOrderBy) { + CollectionReference collection = Collection(); + Query query = collection.OrderBy("foo"); + + const char* reason = + "Too many arguments provided to startAt(). The number of arguments must " + "be less than or equal to the number of orderBy() clauses."; + try { + query.StartAt({FieldValue::Integer(1), FieldValue::Integer(2)}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + query.OrderBy("bar").StartAt({FieldValue::Integer(1), + FieldValue::Integer(2), + FieldValue::Integer(3)}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } +} + +TEST_F(ValidationTest, QueryOrderByKeyBoundsMustBeStringsWithoutSlashes) { + CollectionReference collection = Collection(); + Query query = collection.OrderBy(FieldPath::DocumentId()); + try { + query.StartAt({FieldValue::Integer(1)}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. Expected a string for document ID in startAt(), but " + "got 1.", + exception.what()); + } + try { + query.StartAt({FieldValue::String("foo/bar")}); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying a collection and ordering by " + "FieldPath.documentId(), the value passed to startAt() must be a plain " + "document ID, but 'foo/bar' contains a slash.", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithDifferentInequalityFieldsFail) { + try { + Collection() + .WhereGreaterThan("x", FieldValue::Integer(32)) + .WhereLessThan("y", FieldValue::String("cat")); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "All where filters other than whereEqualTo() must be on the same " + "field. But you have filters on 'x' and 'y'", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithInequalityDifferentThanFirstOrderByFail) { + CollectionReference collection = Collection(); + const char* reason = + "Invalid query. You have an inequality where filter (whereLessThan(), " + "whereGreaterThan(), etc.) on field 'x' and so you must also have 'x' as " + "your first orderBy() field, but your first orderBy() is currently on " + "field 'y' instead."; + try { + collection.WhereGreaterThan("x", FieldValue::Integer(32)).OrderBy("y"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + collection.OrderBy("y").WhereGreaterThan("x", FieldValue::Integer(32)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + collection.WhereGreaterThan("x", FieldValue::Integer(32)) + .OrderBy("y") + .OrderBy("x"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } + try { + collection.OrderBy("y").OrderBy("x").WhereGreaterThan( + "x", FieldValue::Integer(32)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ(reason, exception.what()); + } +} + +TEST_F(ValidationTest, QueriesWithMultipleArrayContainsFiltersFail) { + try { + Collection() + .WhereArrayContains("foo", FieldValue::Integer(1)) + .WhereArrayContains("foo", FieldValue::Integer(2)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid Query. You cannot use more than one 'array_contains' filter.", + exception.what()); + } +} + +TEST_F(ValidationTest, QueriesMustNotSpecifyStartingOrEndingPointAfterOrderBy) { + CollectionReference collection = Collection(); + Query query = collection.OrderBy("foo"); + try { + query.StartAt({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.startAt() or " + "Query.startAfter() before calling Query.orderBy().", + exception.what()); + } + try { + query.StartAfter({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.startAt() or " + "Query.startAfter() before calling Query.orderBy().", + exception.what()); + } + try { + query.EndAt({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.endAt() or " + "Query.endBefore() before calling Query.orderBy().", + exception.what()); + } + try { + query.EndBefore({FieldValue::Integer(1)}).OrderBy("bar"); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You must not call Query.endAt() or " + "Query.endBefore() before calling Query.orderBy().", + exception.what()); + } +} + +TEST_F(ValidationTest, + QueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences) { + CollectionReference collection = Collection(); + try { + collection.WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("")); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying with FieldPath.documentId() you must " + "provide a valid document ID, but it was an empty string.", + exception.what()); + } + + try { + collection.WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("foo/bar/baz")); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying a collection by FieldPath.documentId() " + "you must provide a plain document ID, but 'foo/bar/baz' contains a " + "'/' character.", + exception.what()); + } + + try { + collection.WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::Integer(1)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. When querying with FieldPath.documentId() you must " + "provide a valid String or DocumentReference, but it was of type: " + "java.lang.Long", + exception.what()); + } + + try { + collection.WhereArrayContains(FieldPath::DocumentId(), + FieldValue::Integer(1)); + FAIL() << "should throw exception"; + } catch (const FirestoreException& exception) { + EXPECT_STREQ( + "Invalid query. You can't perform 'array_contains' queries on " + "FieldPath.documentId().", + exception.what()); + } +} + +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/write_batch_test.cc b/firestore/src/tests/write_batch_test.cc new file mode 100644 index 0000000000..69c7cf8abf --- /dev/null +++ b/firestore/src/tests/write_batch_test.cc @@ -0,0 +1,314 @@ +#include + +#include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "firestore/src/tests/util/event_accumulator.h" +#if defined(__ANDROID__) +#include "firestore/src/android/write_batch_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#endif // defined(__ANDROID__) + +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +// These test cases are in sync with native iOS client SDK test +// Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm +// and native Android client SDK test +// firebase_firestore/tests/integration_tests/src/com/google/firebase/firestore/WriteBatchTest.java +// The test cases between the two native client SDK divert quite a lot. The port +// here is an effort to do a superset and cover both cases. + +namespace firebase { +namespace firestore { + +using WriteBatchCommonTest = testing::Test; + +using WriteBatchTest = FirestoreIntegrationTest; + +TEST_F(WriteBatchTest, TestSupportEmptyBatches) { + Await(firestore()->batch().Commit()); +} + +TEST_F(WriteBatchTest, TestSetDocuments) { + DocumentReference doc = Document(); + Await(firestore() + ->batch() + .Set(doc, MapFieldValue{{"a", FieldValue::String("b")}}) + .Set(doc, MapFieldValue{{"c", FieldValue::String("d")}}) + .Set(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"foo", FieldValue::String("bar")}})); +} + +TEST_F(WriteBatchTest, TestSetDocumentWithMerge) { + DocumentReference doc = Document(); + Await(firestore() + ->batch() + .Set(doc, + MapFieldValue{ + {"a", FieldValue::String("b")}, + {"nested", + FieldValue::Map({{"a", FieldValue::String("remove")}})}}, + SetOptions::Merge()) + .Commit()); + Await(firestore() + ->batch() + .Set(doc, + MapFieldValue{ + {"c", FieldValue::String("d")}, + {"ignore", FieldValue::Boolean(true)}, + {"nested", + FieldValue::Map({{"c", FieldValue::String("d")}})}}, + SetOptions::MergeFields({"c", "nested"})) + .Commit()); + Await(firestore() + ->batch() + .Set(doc, + MapFieldValue{ + {"e", FieldValue::String("f")}, + {"nested", FieldValue::Map( + {{"e", FieldValue::String("f")}, + {"ignore", FieldValue::Boolean(true)}})}}, + SetOptions::MergeFieldPaths({{"e"}, {"nested", "e"}})) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT( + snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::String("b")}, + {"c", FieldValue::String("d")}, + {"e", FieldValue::String("f")}, + {"nested", FieldValue::Map({{"c", FieldValue::String("d")}, + {"e", FieldValue::String("f")}})}})); +} + +TEST_F(WriteBatchTest, TestUpdateDocuments) { + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + Await(firestore() + ->batch() + .Update(doc, MapFieldValue{{"baz", FieldValue::Integer(42)}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"foo", FieldValue::String("bar")}, + {"baz", FieldValue::Integer(42)}})); +} + +TEST_F(WriteBatchTest, TestCannotUpdateNonexistentDocuments) { + DocumentReference doc = Document(); + Await(firestore() + ->batch() + .Update(doc, MapFieldValue{{"baz", FieldValue::Integer(42)}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(WriteBatchTest, TestDeleteDocuments) { + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"foo", FieldValue::String("bar")}}); + DocumentSnapshot snapshot = ReadDocument(doc); + + EXPECT_TRUE(snapshot.exists()); + Await(firestore()->batch().Delete(doc).Commit()); + snapshot = ReadDocument(doc); + EXPECT_FALSE(snapshot.exists()); +} + +TEST_F(WriteBatchTest, TestBatchesCommitAtomicallyRaisingCorrectEvents) { + CollectionReference collection = Collection(); + DocumentReference doc_a = collection.Document("a"); + DocumentReference doc_b = collection.Document("b"); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + QuerySnapshot initial_snapshot = accumulator.Await(); + EXPECT_EQ(0, initial_snapshot.size()); + + // Atomically write two documents. + Await(firestore() + ->batch() + .Set(doc_a, MapFieldValue{{"a", FieldValue::Integer(1)}}) + .Set(doc_b, MapFieldValue{{"b", FieldValue::Integer(2)}}) + .Commit()); + + QuerySnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(local_snapshot), + testing::ElementsAre(MapFieldValue{{"a", FieldValue::Integer(1)}}, + MapFieldValue{{"b", FieldValue::Integer(2)}})); + + QuerySnapshot server_snapshot = accumulator.Await(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(server_snapshot), + testing::ElementsAre(MapFieldValue{{"a", FieldValue::Integer(1)}}, + MapFieldValue{{"b", FieldValue::Integer(2)}})); +} + +TEST_F(WriteBatchTest, TestBatchesFailAtomicallyRaisingCorrectEvents) { + CollectionReference collection = Collection(); + DocumentReference doc_a = collection.Document("a"); + DocumentReference doc_b = collection.Document("b"); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + QuerySnapshot initial_snapshot = accumulator.Await(); + EXPECT_EQ(0, initial_snapshot.size()); + + // Atomically write 1 document and update a nonexistent document. + Future future = + firestore() + ->batch() + .Set(doc_a, MapFieldValue{{"a", FieldValue::Integer(1)}}) + .Update(doc_b, MapFieldValue{{"b", FieldValue::Integer(2)}}) + .Commit(); + Await(future); + EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); + EXPECT_EQ(Error::kErrorNotFound, future.error()); + + // Local event with the set document. + QuerySnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(local_snapshot), + testing::ElementsAre(MapFieldValue{{"a", FieldValue::Integer(1)}})); + + // Server event with the set reverted + QuerySnapshot server_snapshot = accumulator.Await(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + EXPECT_EQ(0, server_snapshot.size()); +} + +TEST_F(WriteBatchTest, TestWriteTheSameServerTimestampAcrossWrites) { + CollectionReference collection = Collection(); + DocumentReference doc_a = collection.Document("a"); + DocumentReference doc_b = collection.Document("b"); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&collection, MetadataChanges::kInclude); + QuerySnapshot initial_snapshot = accumulator.Await(); + EXPECT_EQ(0, initial_snapshot.size()); + + // Atomically write two documents with server timestamps. + Await(firestore() + ->batch() + .Set(doc_a, MapFieldValue{{"when", FieldValue::ServerTimestamp()}}) + .Set(doc_b, MapFieldValue{{"when", FieldValue::ServerTimestamp()}}) + .Commit()); + + QuerySnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT( + QuerySnapshotToValues(local_snapshot), + testing::ElementsAre(MapFieldValue{{"when", FieldValue::Null()}}, + MapFieldValue{{"when", FieldValue::Null()}})); + + QuerySnapshot server_snapshot = accumulator.AwaitRemoteEvent(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + EXPECT_EQ(2, server_snapshot.size()); + const FieldValue when = server_snapshot.documents()[0].Get("when"); + EXPECT_EQ(FieldValue::Type::kTimestamp, when.type()); + EXPECT_THAT(QuerySnapshotToValues(server_snapshot), + testing::ElementsAre(MapFieldValue{{"when", when}}, + MapFieldValue{{"when", when}})); +} + +TEST_F(WriteBatchTest, TestCanWriteTheSameDocumentMultipleTimes) { + DocumentReference doc = Document(); + EventAccumulator accumulator; + accumulator.listener()->AttachTo(&doc, MetadataChanges::kInclude); + DocumentSnapshot initial_snapshot = accumulator.Await(); + EXPECT_FALSE(initial_snapshot.exists()); + + Await(firestore() + ->batch() + .Delete(doc) + .Set(doc, MapFieldValue{{"a", FieldValue::Integer(1)}, + {"b", FieldValue::Integer(1)}, + {"when", FieldValue::String("when")}}) + .Update(doc, MapFieldValue{{"b", FieldValue::Integer(2)}, + {"when", FieldValue::ServerTimestamp()}}) + .Commit()); + DocumentSnapshot local_snapshot = accumulator.Await(); + EXPECT_TRUE(local_snapshot.metadata().has_pending_writes()); + EXPECT_THAT(local_snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Integer(1)}, + {"b", FieldValue::Integer(2)}, + {"when", FieldValue::Null()}})); + + DocumentSnapshot server_snapshot = accumulator.Await(); + EXPECT_FALSE(server_snapshot.metadata().has_pending_writes()); + const FieldValue when = server_snapshot.Get("when"); + EXPECT_EQ(FieldValue::Type::kTimestamp, when.type()); + EXPECT_THAT(server_snapshot.GetData(), + testing::ContainerEq(MapFieldValue{{"a", FieldValue::Integer(1)}, + {"b", FieldValue::Integer(2)}, + {"when", when}})); +} + +TEST_F(WriteBatchTest, TestUpdateFieldsWithDots) { + DocumentReference doc = Document(); + WriteDocument(doc, MapFieldValue{{"a.b", FieldValue::String("old")}, + {"c.d", FieldValue::String("old")}}); + Await(firestore() + ->batch() + .Update(doc, MapFieldPathValue{{FieldPath{"a.b"}, + FieldValue::String("new")}}) + .Commit()); + Await(firestore() + ->batch() + .Update(doc, MapFieldPathValue{{FieldPath{"c.d"}, + FieldValue::String("new")}}) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT(snapshot.GetData(), testing::ContainerEq(MapFieldValue{ + {"a.b", FieldValue::String("new")}, + {"c.d", FieldValue::String("new")}})); +} + +TEST_F(WriteBatchTest, TestUpdateNestedFields) { + DocumentReference doc = Document(); + WriteDocument( + doc, MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("old")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("old")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}}); + Await(firestore() + ->batch() + .Update(doc, MapFieldValue({{"a.b", FieldValue::String("new")}})) + .Commit()); + Await(firestore() + ->batch() + .Update(doc, MapFieldPathValue({{FieldPath{"c", "d"}, + FieldValue::String("new")}})) + .Commit()); + DocumentSnapshot snapshot = ReadDocument(doc); + ASSERT_TRUE(snapshot.exists()); + EXPECT_THAT(snapshot.GetData(), + testing::ContainerEq(MapFieldValue{ + {"a", FieldValue::Map({{"b", FieldValue::String("new")}})}, + {"c", FieldValue::Map({{"d", FieldValue::String("new")}})}, + {"e", FieldValue::Map({{"f", FieldValue::String("old")}})}})); +} + +#if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +TEST_F(WriteBatchCommonTest, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST_F(WriteBatchCommonTest, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} + +#endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) + +} // namespace firestore +} // namespace firebase diff --git a/instance_id/src_ios/fake/FIRInstanceID.h b/instance_id/src_ios/fake/FIRInstanceID.h new file mode 100644 index 0000000000..ced17b5891 --- /dev/null +++ b/instance_id/src_ios/fake/FIRInstanceID.h @@ -0,0 +1,330 @@ +// Copyright 2017 Google LLC +// +// 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. + +#ifndef FIREBASE_INSTANCE_ID_CLIENT_CPP_SRC_IOS_FAKE_H_ +#define FIREBASE_INSTANCE_ID_CLIENT_CPP_SRC_IOS_FAKE_H_ + +#ifdef __OBJC__ +#import +#endif // __OBJC__ + +// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. +// Wrap it in our own macro if it's a non-compatible SDK. +#ifndef FIR_SWIFT_NAME +#ifdef __IPHONE_9_3 +#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) +#else +#define FIR_SWIFT_NAME(X) // Intentionally blank. +#endif // #ifdef __IPHONE_9_3 +#endif // #ifndef FIR_SWIFT_NAME + +// C++ enumeration used to inject FIRInstanceIDError values from a C++ test. +enum FIRInstanceIDErrorCode { + kFIRInstanceIDErrorCodeNone = -1, + kFIRInstanceIDErrorCodeUnknown = 0, + kFIRInstanceIDErrorCodeAuthentication = 1, + kFIRInstanceIDErrorCodeNoAccess = 2, + kFIRInstanceIDErrorCodeTimeout = 3, + kFIRInstanceIDErrorCodeNetwork = 4, + kFIRInstanceIDErrorCodeOperationInProgress = 5, + kFIRInstanceIDErrorCodeInvalidRequest = 7, +}; + +// Initialize the mock module. +void FIRInstanceIDInitialize(); + +// Set the next error to be raised by the mock. +void FIRInstanceIDSetNextErrorCode(FIRInstanceIDErrorCode errorCode); + +// Enable / disable blocking on an asynchronous operation. +bool FIRInstanceIDSetBlockingMethodCallsEnable(bool enable); + +// Wait for an operation to start. +bool FIRInstanceIDWaitForBlockedThread(); + +#ifdef __OBJC__ +/** + * @memberof FIRInstanceID + * + * The scope to be used when fetching/deleting a token for Firebase Messaging. + */ +FOUNDATION_EXPORT NSString * __nonnull const kFIRInstanceIDScopeFirebaseMessaging + FIR_SWIFT_NAME(InstanceIDScopeFirebaseMessaging); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + * Called when the system determines that tokens need to be refreshed. + * This method is also called if Instance ID has been reset in which + * case, tokens and FCM topic subscriptions also need to be refreshed. + * + * Instance ID service will throttle the refresh event across all devices + * to control the rate of token updates on application servers. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull kFIRInstanceIDTokenRefreshNotification + FIR_SWIFT_NAME(InstanceIDTokenRefresh); +#else +/** + * Called when the system determines that tokens need to be refreshed. + * This method is also called if Instance ID has been reset in which + * case, tokens and FCM topic subscriptions also need to be refreshed. + * + * Instance ID service will throttle the refresh event across all devices + * to control the rate of token updates on application servers. + */ +FOUNDATION_EXPORT NSString * __nonnull const kFIRInstanceIDTokenRefreshNotification + FIR_SWIFT_NAME(InstanceIDTokenRefreshNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the InstanceID token returns. If + * the call fails we return the appropriate `error code` as described below. + * + * @param token The valid token as returned by InstanceID backend. + * + * @param error The error describing why generating a new token + * failed. See the error codes below for a more detailed + * description. + */ +typedef void(^FIRInstanceIDTokenHandler)( NSString * __nullable token, NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDTokenHandler); + + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the InstanceID `deleteToken` returns. If + * the call fails we return the appropriate `error code` as described below + * + * @param error The error describing why deleting the token failed. + * See the error codes below for a more detailed description. + */ +typedef void(^FIRInstanceIDDeleteTokenHandler)(NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDDeleteTokenHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity is created. If the + * identity wasn't created for some reason we return the appropriate error code. + * + * @param identity A valid identity for the app instance, nil if there was an error + * while creating an identity. + * @param error The error if fetching the identity fails else nil. + */ +typedef void(^FIRInstanceIDHandler)(NSString * __nullable identity, NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity and all the tokens associated + * with it are deleted. Returns a valid error object in case of failure else nil. + * + * @param error The error if deleting the identity and all the tokens associated with + * it fails else nil. + */ +typedef void(^FIRInstanceIDDeleteHandler)(NSError * __nullable error) + FIR_SWIFT_NAME(InstanceIDDeleteHandler); + +/** + * Public errors produced by InstanceID. + */ +typedef NS_ENUM(NSUInteger, FIRInstanceIDError) { + // Http related errors. + + /// Unknown error. + FIRInstanceIDErrorUnknown = 0, + + /// Auth Error -- GCM couldn't validate request from this client. + FIRInstanceIDErrorAuthentication = 1, + + /// NoAccess -- InstanceID service cannot be accessed. + FIRInstanceIDErrorNoAccess = 2, + + /// Timeout -- Request to InstanceID backend timed out. + FIRInstanceIDErrorTimeout = 3, + + /// Network -- No network available to reach the servers. + FIRInstanceIDErrorNetwork = 4, + + /// OperationInProgress -- Another similar operation in progress, + /// bailing this one. + FIRInstanceIDErrorOperationInProgress = 5, + + /// InvalidRequest -- Some parameters of the request were invalid. + FIRInstanceIDErrorInvalidRequest = 7, +} FIR_SWIFT_NAME(InstanceIDError); + +static_assert(static_cast(FIRInstanceIDErrorUnknown) == + static_cast(kFIRInstanceIDErrorCodeUnknown), ""); +static_assert(static_cast(FIRInstanceIDErrorAuthentication) == + static_cast(kFIRInstanceIDErrorCodeAuthentication), ""); +static_assert(static_cast(FIRInstanceIDErrorNoAccess) == + static_cast(kFIRInstanceIDErrorCodeNoAccess), ""); +static_assert(static_cast(FIRInstanceIDErrorTimeout) == + static_cast(kFIRInstanceIDErrorCodeTimeout), ""); +static_assert(static_cast(FIRInstanceIDErrorNetwork) == + static_cast(kFIRInstanceIDErrorCodeNetwork), ""); +static_assert(static_cast(FIRInstanceIDErrorOperationInProgress) == + static_cast(kFIRInstanceIDErrorCodeOperationInProgress), ""); +static_assert(static_cast(FIRInstanceIDErrorInvalidRequest) == + static_cast(kFIRInstanceIDErrorCodeInvalidRequest), ""); + +/** + * The APNS token type for the app. If the token type is set to `UNKNOWN` + * InstanceID will implicitly try to figure out what the actual token type + * is from the provisioning profile. + */ +typedef NS_ENUM(NSInteger, FIRInstanceIDAPNSTokenType) { + /// Unknown token type. + FIRInstanceIDAPNSTokenTypeUnknown, + /// Sandbox token type. + FIRInstanceIDAPNSTokenTypeSandbox, + /// Production token type. + FIRInstanceIDAPNSTokenTypeProd, +} FIR_SWIFT_NAME(InstanceIDAPNSTokenType) + __deprecated_enum_msg("Use FIRMessaging's APNSToken property instead."); + +/** + * Instance ID provides a unique identifier for each app instance and a mechanism + * to authenticate and authorize actions (for example, sending an FCM message). + * + * Instance ID is long lived but, may be reset if the device is not used for + * a long time or the Instance ID service detects a problem. + * If Instance ID is reset, the app will be notified via + * `kFIRInstanceIDTokenRefreshNotification`. + * + * If the Instance ID has become invalid, the app can request a new one and + * send it to the app server. + * To prove ownership of Instance ID and to allow servers to access data or + * services associated with the app, call + * `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. + */ +FIR_SWIFT_NAME(InstanceID) +@interface FIRInstanceID : NSObject + +/** + * FIRInstanceID. + * + * @return A shared instance of FIRInstanceID. + */ ++ (nonnull instancetype)instanceID FIR_SWIFT_NAME(instanceID()); + +#pragma mark - Tokens + +/** + * Returns a Firebase Messaging scoped token for the firebase app. + * + * @return Null Returns null if the device has not yet been registerd with + * Firebase Message else returns a valid token. + */ +- (nullable NSString *)token; + +/** + * Returns a token that authorizes an Entity (example: cloud service) to perform + * an action on behalf of the application identified by Instance ID. + * + * This is similar to an OAuth2 token except, it applies to the + * application instance instead of a user. + * + * This is an asynchronous call. If the token fetching fails for some reason + * we invoke the completion callback with nil `token` and the appropriate + * error. + * + * Note, you can only have one `token` or `deleteToken` call for a given + * authorizedEntity and scope at any point of time. Making another such call with the + * same authorizedEntity and scope before the last one finishes will result in an + * error with code `OperationInProgress`. + * + * @see FIRInstanceID deleteTokenWithAuthorizedEntity:scope:handler: + * + * @param authorizedEntity Entity authorized by the token. + * @param scope Action authorized for authorizedEntity. + * @param options The extra options to be sent with your token request. The + * value for the `apns_token` should be the NSData object + * passed to the UIApplicationDelegate's + * `didRegisterForRemoteNotificationsWithDeviceToken` method. + * The value for `apns_sandbox` should be a boolean (or an + * NSNumber representing a BOOL in Objective C) set to true if + * your app is a debug build, which means that the APNs + * device token is for the sandbox environment. It should be + * set to false otherwise. If the `apns_sandbox` key is not + * provided, an automatically-detected value shall be used. + * @param handler The callback handler which is invoked when the token is + * successfully fetched. In case of success a valid `token` and + * `nil` error are returned. In case of any error the `token` + * is nil and a valid `error` is returned. The valid error + * codes have been documented above. + */ +- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + options:(nullable NSDictionary *)options + handler:(nonnull FIRInstanceIDTokenHandler)handler; + +/** + * Revokes access to a scope (action) for an entity previously + * authorized by `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. + * + * This is an asynchronous call. Call this on the main thread since InstanceID lib + * is not thread safe. In case token deletion fails for some reason we invoke the + * `handler` callback passed in with the appropriate error code. + * + * Note, you can only have one `token` or `deleteToken` call for a given + * authorizedEntity and scope at a point of time. Making another such call with the + * same authorizedEntity and scope before the last one finishes will result in an error + * with code `OperationInProgress`. + * + * @param authorizedEntity Entity that must no longer have access. + * @param scope Action that entity is no longer authorized to perform. + * @param handler The handler that is invoked once the unsubscribe call ends. + * In case of error an appropriate error object is returned + * else error is nil. + */ +- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + handler:(nonnull FIRInstanceIDDeleteTokenHandler)handler; + +#pragma mark - Identity + +/** + * Asynchronously fetch a stable identifier that uniquely identifies the app + * instance. If the identifier has been revoked or has expired, this method will + * return a new identifier. + * + * + * @param handler The handler to invoke once the identifier has been fetched. + * In case of error an appropriate error object is returned else + * a valid identifier is returned and a valid identifier for the + * application instance. + */ +- (void)getIDWithHandler:(nonnull FIRInstanceIDHandler)handler + FIR_SWIFT_NAME(getID(handler:)); + +/** + * Resets Instance ID and revokes all tokens. + * + * This method also triggers a request to fetch a new Instance ID and Firebase Messaging scope + * token. Please listen to kFIRInstanceIDTokenRefreshNotification when the new ID and token are + * ready. + */ +- (void)deleteIDWithHandler:(nonnull FIRInstanceIDDeleteHandler)handler + FIR_SWIFT_NAME(deleteID(handler:)); + +@end + +#endif // __OBJC__ + +#endif // FIREBASE_INSTANCE_ID_CLIENT_CPP_SRC_IOS_FAKE_H_ diff --git a/instance_id/src_ios/fake/FIRInstanceID.mm b/instance_id/src_ios/fake/FIRInstanceID.mm new file mode 100644 index 0000000000..539ae0198c --- /dev/null +++ b/instance_id/src_ios/fake/FIRInstanceID.mm @@ -0,0 +1,158 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "instance_id/src_ios/fake/FIRInstanceID.h" + +#include + +#include + +#import + +#include "testing/reporter_impl.h" + +static FIRInstanceIDErrorCode gNextErrorCode = kFIRInstanceIDErrorCodeNone; +static bool gBlockingEnabled = false; + +static dispatch_semaphore_t gBlocking; +static dispatch_semaphore_t gThreadStarted; +static dispatch_semaphore_t gThreadComplete; + +// Initialize the mock module. +void FIRInstanceIDInitialize() { + gBlocking = dispatch_semaphore_create(0); + gThreadStarted = dispatch_semaphore_create(0); + gThreadComplete = dispatch_semaphore_create(0); + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeNone); +} + +// Set the next error to be raised by the mock. +void FIRInstanceIDSetNextErrorCode(FIRInstanceIDErrorCode errorCode) { + gNextErrorCode = errorCode; +} + +// Retrieve the next error code and clear the current error code. +static FIRInstanceIDErrorCode GetAndClearErrorCode() { + FIRInstanceIDErrorCode errorCode = gNextErrorCode; + gNextErrorCode = kFIRInstanceIDErrorCodeNone; + return errorCode; +} + +// Wait 1 second while trying to acquire a semaphore, returning false on timeout. +static bool WaitForSemaphore(dispatch_semaphore_t semaphore) { + static const int64_t kSemaphoreWaitTimeoutNanoseconds = 1000000000 /* 1s */; + return dispatch_semaphore_wait(semaphore, + dispatch_time(DISPATCH_TIME_NOW, + kSemaphoreWaitTimeoutNanoseconds)) == 0; +} + +// Enable / disable blocking on an asynchronous operation. +bool FIRInstanceIDSetBlockingMethodCallsEnable(bool enable) { + bool stateChanged = gBlockingEnabled != enable; + if (stateChanged) { + if (enable) { + gBlockingEnabled = enable; + } else { + gBlockingEnabled = enable; + dispatch_semaphore_signal(gBlocking); + if (!WaitForSemaphore(gThreadComplete)) return false; + } + } + return true; +} + +// Wait for an operation to start. +bool FIRInstanceIDWaitForBlockedThread() { + return WaitForSemaphore(gThreadStarted); +} + +// Run a block on a background thread. +static void RunBlockOnBackgroundThread(void (^block)(NSError* _Nullable error)) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_semaphore_signal(gThreadStarted); + int error_code = GetAndClearErrorCode(); + NSError * _Nullable error = nil; + if (error_code != kFIRInstanceIDErrorCodeNone) { + NSDictionary* userInfo = @{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Mock error code %d", error_code] + }; + error = [NSError errorWithDomain:@"Mock error" + code:error_code + userInfo:userInfo]; + } + else if (gBlockingEnabled) { + if (!WaitForSemaphore(gBlocking)) { + error = [NSError errorWithDomain:@"Timeout" + code:-1 + userInfo:nil]; + } + } + block(error); + dispatch_semaphore_signal(gThreadComplete); + }); +} + +@implementation FIRInstanceID + ++ (instancetype)instanceID { + if (GetAndClearErrorCode() != kFIRInstanceIDErrorCodeNone) return nil; + FakeReporter->AddReport("FirebaseInstanceId.construct", {}); + return [[FIRInstanceID alloc] init]; +} + +- (NSString*)token { + FakeReporter->AddReport("FirebaseInstanceId.getToken", {}); + return @"FakeToken"; +} + +- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + options:(nullable NSDictionary *)options + handler:(nonnull FIRInstanceIDTokenHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) { + FakeReporter->AddReport("FirebaseInstanceId.getToken", + { authorizedEntity.UTF8String, scope.UTF8String }); + } + handler(error ? nil : @"FakeToken", error); + }); +} + +- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope + handler:(nonnull FIRInstanceIDDeleteTokenHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) { + FakeReporter->AddReport("FirebaseInstanceId.deleteToken", + { authorizedEntity.UTF8String, scope.UTF8String }); + } + handler(error); + }); +} + +- (void)getIDWithHandler:(nonnull FIRInstanceIDHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) FakeReporter->AddReport("FirebaseInstanceId.getId", {}); + handler(error ? nil : @"FakeId", error); + }); +} + +- (void)deleteIDWithHandler:(nonnull FIRInstanceIDDeleteHandler)handler { + RunBlockOnBackgroundThread(^(NSError *_Nullable error) { + if (!error) FakeReporter->AddReport("FirebaseInstanceId.deleteId", {}); + handler(error); + }); +} + +@end diff --git a/instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java b/instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java new file mode 100644 index 0000000000..2715dd0b84 --- /dev/null +++ b/instance_id/src_java/fake/com/google/firebase/iid/FirebaseInstanceId.java @@ -0,0 +1,187 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.iid; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.testing.cppsdk.FakeReporter; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.GuardedBy; + +/** + * Mock FirebaseInstanceId. + */ +public class FirebaseInstanceId { + + /** + * If set, {@link throwExceptionOrBlockThreadIfEnabled} will throw an IOException or + * IllegalStateException (from the constructor) with this message. + */ + private static String exceptionErrorMessage = null; + + /** If set, all method calls will block until this is signalled. */ + @GuardedBy("threadStarted") + private static CountDownLatch threadBlocker = null; + + /** Sempahore used to wait for a thread to start. */ + private static final Semaphore threadStarted = new Semaphore(0); + + /** Semaphore used to wait for the woken up thread to finish. */ + private static final Semaphore threadFinished = new Semaphore(0); + + /** + * Set a message which will be used to throw an Exception from all method calls. + * Clear the message by setting the value to null. + */ + public static void setThrowExceptionMessage(String errorMessage) { + exceptionErrorMessage = errorMessage; + } + + /** Make all operations block indefinitely until this flag is cleared. */ + public static boolean setBlockingMethodCallsEnable(boolean enable) { + boolean stateChanged = false; + synchronized (threadStarted) { + if ((enable && threadBlocker == null) || (!enable && threadBlocker != null)) { + stateChanged = true; + } + if (enable && stateChanged) { + threadBlocker = new CountDownLatch(1); + threadStarted.drainPermits(); + threadFinished.drainPermits(); + } + } + if (stateChanged && !enable) { + synchronized (threadStarted) { + threadBlocker.countDown(); + threadBlocker = null; + } + try { + boolean acquired = threadFinished.tryAcquire(1, 1, TimeUnit.SECONDS); + if (!acquired) { + return false; + } + } catch (InterruptedException e) { + return false; + } + } + return true; + } + + /** Wait for a thread to start and wait on {@link threadBlocker}. */ + public static boolean waitForBlockedThread() { + boolean acquired = false; + try { + acquired = threadStarted.tryAcquire(1, 1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + return acquired; + } + + /** Block the thread if enabled by {@link setBlockingMethodCallsEnable}. */ + private static void blockThreadIfEnabled() { + threadStarted.release(); + try { + CountDownLatch latch = null; + synchronized (threadStarted) { + latch = threadBlocker; + } + if (latch != null) { + latch.await(); + } + } catch (InterruptedException e) { + return; + } + } + + /** Signal thread completion to continue execution in {@link setBlockingMethodCallsEnable}. */ + private static void signalThreadCompletion() { + threadFinished.release(); + } + + /** + * Throw an exception or block the thread if enabled by {@link setThrowExceptionMessage} or + * {@link setBlockingMethodCallsEnabled} respectively. + */ + private static void throwExceptionOrBlockThreadIfEnabled() throws IOException { + if (exceptionErrorMessage != null) { + throw new IOException(exceptionErrorMessage); + } + blockThreadIfEnabled(); + } + + // Fake interface below. + + private FirebaseInstanceId() { + if (exceptionErrorMessage != null) { + throw new IllegalStateException(exceptionErrorMessage); + } + FakeReporter.addReport("FirebaseInstanceId.construct"); + } + + public String getId() { + try { + blockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.getId"); + } finally { + signalThreadCompletion(); + } + return "FakeId"; + } + + public long getCreationTime() { + try { + blockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.getCreationTime"); + } finally { + signalThreadCompletion(); + } + return 1512000287000L; // 11/29/17 16:04:47 + } + + public void deleteInstanceId() throws IOException { + try { + throwExceptionOrBlockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.deleteId"); + } finally { + signalThreadCompletion(); + } + } + + public String getToken(String authorizedEntity, String scope) throws IOException { + try { + throwExceptionOrBlockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.getToken", authorizedEntity, scope); + } finally { + signalThreadCompletion(); + } + return "FakeToken"; + } + + public void deleteToken(String authorizedEntity, String scope) throws IOException { + try { + throwExceptionOrBlockThreadIfEnabled(); + FakeReporter.addReport("FirebaseInstanceId.deleteToken", authorizedEntity, scope); + } finally { + signalThreadCompletion(); + } + } + + public static synchronized FirebaseInstanceId getInstance(FirebaseApp app) { + return new FirebaseInstanceId(); + } +} diff --git a/instance_id/tests/CMakeLists.txt b/instance_id/tests/CMakeLists.txt new file mode 100644 index 0000000000..3caf5d5944 --- /dev/null +++ b/instance_id/tests/CMakeLists.txt @@ -0,0 +1,36 @@ +# Copyright 2019 Google LLC +# +# 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. + +firebase_cpp_cc_test( + firebase_instance_id_test + SOURCES + ${FIREBASE_SOURCE_DIR}/instance_id/tests/instance_id_test.cc + DEPENDS + firebase_app_for_testing + firebase_instance_id + firebase_testing +) + +firebase_cpp_cc_test_on_ios( + firebase_instance_id_test + HOST + firebase_app_for_testing_ios + SOURCES + ${FIREBASE_SOURCE_DIR}/instance_id/tests/instance_id_test.cc + ${FIREBASE_SOURCE_DIR}/instance_id/src_ios/fake/FIRInstanceID.mm + DEPENDS + firebase_instance_id + firebase_testing +) + diff --git a/instance_id/tests/instance_id_test.cc b/instance_id/tests/instance_id_test.cc new file mode 100644 index 0000000000..a4aea63d1e --- /dev/null +++ b/instance_id/tests/instance_id_test.cc @@ -0,0 +1,546 @@ +// Copyright 2017 Google LLC +// +// 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. + +// WARNING: Some Code from this file is included verbatim in the C++ +// documentation. Only change existing code if it is safe to release +// to the public. Otherwise, a tech writer may make an unrelated +// modification, regenerate the docs, and unwittingly release an +// unannounced modification to the public. + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#include +#define __ANDROID__ +#include + +#include + +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#if defined(__APPLE__) +#include "TargetConditionals.h" +#endif // defined(__APPLE__) + +// [START instance_id_includes] +#include "app/src/include/firebase/app.h" +#include "app/src/include/firebase/internal/platform.h" +#include "app/src/log.h" +#include "app/src/time.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "instance_id/src/include/firebase/instance_id.h" +// [END instance_id_includes] +#if TARGET_OS_IPHONE +#include "instance_id/src_ios/fake/FIRInstanceID.h" +#endif // TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" + +using testing::Eq; +using testing::IsNull; +using testing::MatchesRegex; +using testing::NotNull; + +namespace firebase { +namespace instance_id { + +class InstanceIdTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + AppOptions options; + options.set_messaging_sender_id("123456"); +#if TARGET_OS_IPHONE + FIRInstanceIDInitialize(); +#endif // TARGET_OS_IPHONE + reporter_.reset(); + app_ = testing::CreateApp(); + } + + void TearDown() override { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage(nullptr); + SetBlockingMethodCallsEnable(false); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + firebase::testing::cppsdk::ConfigReset(); + delete app_; + app_ = nullptr; + EXPECT_THAT(reporter_.getFakeReports(), Eq(reporter_.getExpectations())); + } + + void AddExpectationAndroid(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kAndroid, + args); + } + + void AddExpectationIos(const char* fake, + std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kIos, args); + } + + void AddExpectationAndroidIos(const char* fake, + std::initializer_list args) { + AddExpectationAndroid(fake, args); + AddExpectationIos(fake, args); + } + + // Wait for a future up to the specified number of milliseconds. + template + static void WaitForFutureWithTimeout( + const Future& future, + int timeout_milliseconds = kFutureTimeoutMilliseconds, + FutureStatus expected_status = kFutureStatusComplete) { + while (future.status() != expected_status && timeout_milliseconds-- > 0) { + ::firebase::internal::Sleep(1); + } + } + + // Validate that a future completed successfully and has the specified + // result. + template + static void CheckSuccessWithValue(const Future& future, const T& result) { + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.error(), Eq(instance_id::kErrorNone)); + EXPECT_THAT(*future.result(), Eq(result)); + } + + // Validate that a future completed successfully. + static void CheckSuccess(const Future& future) { + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.status(), Eq(kFutureStatusComplete)); + EXPECT_THAT(future.error(), Eq(instance_id::kErrorNone)); + } + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Find the mock FirebaseInstanceId class. + static void GetMockClass( + const std::function& retrieved_class) { + JNIEnv* env = firebase::testing::cppsdk::GetTestJniEnv(); + jclass clazz = env->FindClass("com/google/firebase/iid/FirebaseInstanceId"); + retrieved_class(env, clazz); + env->DeleteLocalRef(clazz); + } +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + // Set the exception message to throw from the next method call to the fake. + static void SetThrowExceptionMessage(const char* message) { + GetMockClass([&message](JNIEnv* env, jclass clazz) { + jmethodID methodId = env->GetStaticMethodID( + clazz, "setThrowExceptionMessage", "(Ljava/lang/String;)V"); + jobject stringobj = message ? env->NewStringUTF(message) : nullptr; + env->CallStaticVoidMethod(clazz, methodId, stringobj); + if (env->ExceptionCheck()) env->ExceptionClear(); + if (stringobj) env->DeleteLocalRef(stringobj); + }); + } +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + + // Enable / disable indefinite blocking of all mock method calls. + static bool SetBlockingMethodCallsEnable(bool enable) { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + bool successful = false; + GetMockClass([&enable, &successful](JNIEnv* env, jclass clazz) { + jmethodID methodId = + env->GetStaticMethodID(clazz, "setBlockingMethodCallsEnable", "(Z)Z"); + successful = env->CallStaticBooleanMethod(clazz, methodId, enable); + if (env->ExceptionCheck()) env->ExceptionClear(); + }); + return successful; +#elif TARGET_OS_IPHONE + return FIRInstanceIDSetBlockingMethodCallsEnable(enable); +#endif + return false; + } + + // Wait for the worker thread to start, returning true if the thread started, + // false otherwise. + static bool WaitForBlockedThread() { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + bool successful = false; + GetMockClass([&successful](JNIEnv* env, jclass clazz) { + jmethodID methodId = + env->GetStaticMethodID(clazz, "waitForBlockedThread", "()Z"); + successful = env->CallStaticBooleanMethod(clazz, methodId); + if (env->ExceptionCheck()) env->ExceptionClear(); + }); + return successful; +#elif TARGET_OS_IPHONE + return FIRInstanceIDWaitForBlockedThread(); +#endif + return false; + } + + // Validate the specified future handle is invalid. + template + static void ExpectInvalidFuture(const Future& future) { + EXPECT_THAT(future.status(), Eq(kFutureStatusInvalid)); + EXPECT_THAT(future.error_message(), IsNull()); + } + + App* app_ = nullptr; + firebase::testing::cppsdk::Reporter reporter_; + static const char* const kTokenEntity; + static const char* const kTokenScope; + static const char* const kTokenScopeAll; + static const int kMicrosecondsPerMillisecond; + // Default time to wait for future status changes. + static const int kFutureTimeoutMilliseconds; +}; + +const char* const InstanceIdTest::kTokenEntity = "an_entity"; +const char* const InstanceIdTest::kTokenScope = "a_scope"; +const char* const InstanceIdTest::kTokenScopeAll = "*"; +const int InstanceIdTest::kMicrosecondsPerMillisecond = 1000; +const int InstanceIdTest::kFutureTimeoutMilliseconds = 1000; + +// Validate creation of an InstanceId instance. +TEST_F(InstanceIdTest, TestCreate) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id, NotNull()); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); + delete instance_id; +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +// Test creation that fails. +TEST_F(InstanceIdTest, TestCreateWithError) { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("Failed to initialize"); +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeUnknown); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id, IsNull()); + EXPECT_THAT(init_result, Eq(kInitResultFailedMissingDependency)); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +// Ensure the retrieving the an InstanceId from the same app returns the same +// instance. +TEST_F(InstanceIdTest, TestCreateAndGet) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id1 = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id1, NotNull()); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); + auto* instance_id2 = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_THAT(instance_id2, Eq(instance_id1)); + delete instance_id1; +} + +// Validate InstanceId instance is destroyed when the corresponding app is +// destroyed. +// NOTE: It's not possible to execute this test on iOS as we can only create an +// instance ID object for the default app. +#if !TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestCreateAndDestroyApp) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + const char* kAppNames[] = {"named_app1", "named_app2"}; + auto* app = testing::CreateApp(testing::MockAppOptions(), kAppNames[0]); + auto* instance_id1 = InstanceId::GetInstanceId(app, &init_result); + EXPECT_THAT(instance_id1, NotNull()); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); + + // Temporarily disable LogAssert() which causes the application to assert. + void* log_callback_data; + LogCallback log_callback = LogGetCallback(&log_callback_data); + LogSetCallback( + [](LogLevel log_level, const char* log_message, void* callback_data) { + if (log_level == kLogLevelAssert) { + ASSERT_THAT( + log_message, + MatchesRegex( + "InstanceId object 0x[0-9A-Fa-f]+ should be " + "deleted before the App 0x[0-9A-Fa-f]+ it depends upon.")); + log_level = kLogLevelError; + } + reinterpret_cast(callback_data)(log_level, log_message, + nullptr); + }, + reinterpret_cast(log_callback)); + + delete app; // This should delete instance_id1's internal data, not + // instance_id1 itself. + EXPECT_THAT(instance_id1, NotNull()); + delete instance_id1; + + LogSetCallback(log_callback, log_callback_data); + + app = testing::CreateApp(testing::MockAppOptions(), kAppNames[1]); + // Validate the new app instance yields a new Instance ID object. + auto* instance_id2 = InstanceId::GetInstanceId(app, &init_result); + EXPECT_THAT(std::string(instance_id2->app().name()), + Eq(std::string(kAppNames[1]))); + EXPECT_THAT(init_result, Eq(kInitResultSuccess)); +} +#endif // !TARGET_OS_IPHONE + +TEST_F(InstanceIdTest, TestGetCreationTime) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); +#if !TARGET_OS_IPHONE + // At the moment creation_time() is not exposed on iOS. + AddExpectationAndroidIos("FirebaseInstanceId.getCreationTime", {}); +#endif // !TARGET_OS_IPHONE + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_THAT(instance_id->creation_time(), Eq(1512000287000)); +#else + EXPECT_THAT(instance_id->creation_time(), Eq(0)); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + delete instance_id; +} + +#if FIREBASE_PLATFORM_MOBILE +TEST_F(InstanceIdTest, TestGetId) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.getId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + const std::string expected_value("FakeId"); + CheckSuccessWithValue(instance_id->GetId(), expected_value); + CheckSuccessWithValue(instance_id->GetIdLastResult(), expected_value); + delete instance_id; +} +#endif // FIREBASE_PLATFORM_MOBILE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestGetIdTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.getId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->GetId(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +TEST_F(InstanceIdTest, TestDeleteId) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.deleteId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + CheckSuccess(instance_id->DeleteId()); + CheckSuccess(instance_id->DeleteIdLastResult()); + delete instance_id; +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteIdFailed) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + Error expected_error = kErrorUnknown; +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("Error while reading ID"); +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeNoAccess); + expected_error = kErrorNoAccess; +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + auto future = instance_id->DeleteId(); + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.error(), Eq(expected_error)); + EXPECT_THAT(future.error_message(), NotNull()); + delete instance_id; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteIdTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.deleteId", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->DeleteId(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if FIREBASE_PLATFORM_MOBILE +TEST_F(InstanceIdTest, TestGetTokenEntityScope) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.getToken", + {kTokenEntity, kTokenScope}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + const std::string expected_value("FakeToken"); + CheckSuccessWithValue(instance_id->GetToken(kTokenEntity, kTokenScope), + expected_value); + CheckSuccessWithValue(instance_id->GetTokenLastResult(), expected_value); + delete instance_id; +} + +TEST_F(InstanceIdTest, TestGetToken) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.getToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + const std::string expected_value("FakeToken"); + CheckSuccessWithValue(instance_id->GetToken(), expected_value); + CheckSuccessWithValue(instance_id->GetTokenLastResult(), expected_value); + delete instance_id; +} + +// Sample code that creates an InstanceId for the default app and gets a token. +TEST_F(InstanceIdTest, TestGetTokenSample) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.getToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + // [START instance_id_get_token] + firebase::InitResult init_result; + auto* instance_id_object = firebase::instance_id::InstanceId::GetInstanceId( + firebase::App::GetInstance(), &init_result); + instance_id_object->GetToken().OnCompletion( + [](const firebase::Future& future) { + if (future.status() == kFutureStatusComplete && + future.error() == firebase::instance_id::kErrorNone) { + printf("Instance ID Token %s\n", future.result()->c_str()); + } + }); + // [END instance_id_get_token] + // WaitForFutureWithTimeout(instance_id_object->GetTokenLastResult()); + CheckSuccessWithValue(instance_id_object->GetTokenLastResult(), + std::string("FakeToken")); + delete instance_id_object; +} +#endif // FIREBASE_PLATFORM_MOBILE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestGetTokenFailed) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + Error expected_error = kErrorUnknown; +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("INSTANCE_ID_RESET"); + expected_error = kErrorIdInvalid; +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeAuthentication); + expected_error = kErrorAuthentication; +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + auto future = instance_id->GetToken(); + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.error(), Eq(expected_error)); + EXPECT_THAT(future.error_message(), NotNull()); + delete instance_id; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestGetTokenTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.getToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->GetToken(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +TEST_F(InstanceIdTest, TestDeleteTokenEntityScope) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos("FirebaseInstanceId.deleteToken", + {kTokenEntity, kTokenScope}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + CheckSuccess(instance_id->DeleteToken(kTokenEntity, kTokenScope)); + CheckSuccess(instance_id->DeleteTokenLastResult()); + delete instance_id; +} + +TEST_F(InstanceIdTest, TestDeleteToken) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.deleteToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + CheckSuccess(instance_id->DeleteToken()); + CheckSuccess(instance_id->DeleteTokenLastResult()); + delete instance_id; +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteTokenFailed) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + SetThrowExceptionMessage("SERVICE_NOT_AVAILABLE"); +#else + FIRInstanceIDSetNextErrorCode(kFIRInstanceIDErrorCodeNoAccess); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + auto future = instance_id->DeleteToken(); + WaitForFutureWithTimeout(future); + EXPECT_THAT(future.error(), Eq(kErrorNoAccess)); + EXPECT_THAT(future.error_message(), NotNull()); + delete instance_id; +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE +TEST_F(InstanceIdTest, TestDeleteTokenTeardown) { + AddExpectationAndroidIos("FirebaseInstanceId.construct", {}); + AddExpectationAndroidIos( + "FirebaseInstanceId.deleteToken", + {app_->options().messaging_sender_id(), kTokenScopeAll}); + InitResult init_result; + auto* instance_id = InstanceId::GetInstanceId(app_, &init_result); + EXPECT_TRUE(SetBlockingMethodCallsEnable(true)); + auto future = instance_id->DeleteToken(); + EXPECT_TRUE(WaitForBlockedThread()); + EXPECT_THAT(future.status(), Eq(kFutureStatusPending)); + delete instance_id; + EXPECT_TRUE(SetBlockingMethodCallsEnable(false)); + ExpectInvalidFuture(future); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || TARGET_OS_IPHONE + +} // namespace instance_id +} // namespace firebase diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java b/messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java new file mode 100644 index 0000000000..05e86e6466 --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/MessageForwardingServiceTest.java @@ -0,0 +1,82 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.content.Context; +import android.content.Intent; +import com.google.firebase.messaging.cpp.MessageWriter; +import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(GoogleRobolectricTestRunner.class) +public final class MessageForwardingServiceTest { + + @Mock private Context context; + @Mock private MessageWriter messageWriter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testHandleIntent() throws Exception { + Intent intent = new Intent(MessageForwardingService.ACTION_REMOTE_INTENT); + intent.putExtra("from", "from"); + intent.putExtra("google.message_id", "id"); + ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteMessage.class); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verify(messageWriter).writeMessage(any(), captor.capture(), eq(true), eq(null)); + RemoteMessage message = captor.getValue(); + assertThat(message.getMessageId()).isEqualTo("id"); + assertThat(message.getFrom()).isEqualTo("from"); + } + + @Test + public void testHandleIntent_noFrom() throws Exception { + Intent intent = new Intent(MessageForwardingService.ACTION_REMOTE_INTENT); + intent.putExtra("from", "from"); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verifyZeroInteractions(messageWriter); + } + + @Test + public void testHandleIntent_noId() throws Exception { + Intent intent = new Intent(MessageForwardingService.ACTION_REMOTE_INTENT); + intent.putExtra("google.message_id", "id"); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verifyZeroInteractions(messageWriter); + } + + @Test + public void testHandleIntent_wrongAction() throws Exception { + Intent intent = new Intent("wrong_action"); + intent.putExtra("from", "from"); + intent.putExtra("google.message_id", "id"); + MessageForwardingService.handleIntent(context, intent, messageWriter); + verifyZeroInteractions(messageWriter); + } +} diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java b/messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java new file mode 100644 index 0000000000..64ce0ad5c2 --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/RemoteMessageUtil.java @@ -0,0 +1,31 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import android.os.Bundle; + +/** */ +public class RemoteMessageUtil { + + private RemoteMessageUtil() {} // Utility class. + + public static RemoteMessage remoteMessage(Bundle bundle) { + return new RemoteMessage(bundle); + } + + public static SendException sendException(String reason) { + return new SendException(reason); + } +} diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java new file mode 100644 index 0000000000..ff7db3b4ed --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/ListenerServiceTest.java @@ -0,0 +1,86 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging.cpp; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.verify; + +import android.net.Uri; +import android.os.Bundle; +import com.google.firebase.messaging.RemoteMessage; +import com.google.firebase.messaging.RemoteMessageUtil; +import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(GoogleRobolectricTestRunner.class) +public final class ListenerServiceTest { + + @Mock private MessageWriter messageWriter; + + private ListenerService listenerService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + listenerService = new ListenerService(messageWriter); + } + + @Test + public void testOnDeletedMessages() throws Exception { + listenerService.onDeletedMessages(); + verify(messageWriter) + .writeMessageEventToInternalStorage( + eq(listenerService), + (String) isNull(), + eq(ListenerService.MESSAGE_TYPE_DELETED), + (String) isNull()); + } + + @Test + public void testOnMessageReceived() { + RemoteMessage message = RemoteMessageUtil.remoteMessage(new Bundle()); + listenerService.onMessageReceived(message); + verify(messageWriter).writeMessage(any(), eq(message), eq(false), (Uri) isNull()); + } + + @Test + public void testOnMessageSent() { + listenerService.onMessageSent("message_id"); + verify(messageWriter) + .writeMessageEventToInternalStorage( + eq(listenerService), + eq("message_id"), + eq(ListenerService.MESSAGE_TYPE_SEND_EVENT), + (String) isNull()); + } + + @Test + public void testOnSendError() { + listenerService.onSendError( + "message_id", RemoteMessageUtil.sendException("service_not_available")); + verify(messageWriter) + .writeMessageEventToInternalStorage( + eq(listenerService), + eq("message_id"), + eq(ListenerService.MESSAGE_TYPE_SEND_ERROR), + eq("com.google.firebase.messaging.SendException: service_not_available")); + } +} diff --git a/messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java new file mode 100644 index 0000000000..7621a0b0be --- /dev/null +++ b/messaging/src/android/javatests/com/google/firebase/messaging/cpp/MessageWriterTest.java @@ -0,0 +1,91 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging.cpp; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Bundle; +import com.google.common.io.ByteStreams; +import com.google.firebase.messaging.RemoteMessage; +import com.google.firebase.messaging.RemoteMessageUtil; +import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(GoogleRobolectricTestRunner.class) +public final class MessageWriterTest { + + private static final Path STORAGE_FILE_PATH = Paths.get("/tmp/" + MessageWriter.STORAGE_FILE); + + @Mock private Context context; + private MessageWriter messageWriter; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + messageWriter = new MessageWriter(); + Files.deleteIfExists(STORAGE_FILE_PATH); + when(context.openFileOutput(eq(MessageWriter.LOCK_FILE), anyInt())) + .thenReturn(new FileOutputStream("/tmp/" + MessageWriter.LOCK_FILE)); + when(context.openFileOutput(eq(MessageWriter.STORAGE_FILE), anyInt())) + .thenReturn(new FileOutputStream(STORAGE_FILE_PATH.toFile(), true)); + } + + @Test + public void testMessageWriter() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString("from", "my_from"); + bundle.putString("google.message_id", "my_message_id"); + bundle.putString("some_data", "my_data"); + bundle.putString("collapse_key", "a_key"); + bundle.putString("google.priority", "high"); + bundle.putString("google.original_priority", "normal"); + bundle.putLong("google.sent_time", 1234); + bundle.putInt("google.ttl", 8765); + RemoteMessage message = RemoteMessageUtil.remoteMessage(bundle); + messageWriter.writeMessage(context, message, false, null); + ByteBuffer byteBuffer = ByteBuffer.wrap(readStorageFile()).order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.getInt(); // Discard size. + SerializedEvent event = SerializedEvent.getRootAsSerializedEvent(byteBuffer); + SerializedMessage result = (SerializedMessage) event.event(new SerializedMessage()); + assertThat(result.from()).isEqualTo("my_from"); + assertThat(result.messageId()).isEqualTo("my_message_id"); + assertThat(result.collapseKey()).isEqualTo("a_key"); + assertThat(result.priority()).isEqualTo("high"); + assertThat(result.originalPriority()).isEqualTo("normal"); + assertThat(result.sentTime()).isEqualTo(1234); + assertThat(result.timeToLive()).isEqualTo(8765); + assertThat(result.data(0).key()).isEqualTo("some_data"); + assertThat(result.data(0).value()).isEqualTo("my_data"); + } + + private byte[] readStorageFile() throws Exception { + return ByteStreams.toByteArray(new FileInputStream(STORAGE_FILE_PATH.toFile())); + } +} diff --git a/messaging/src/ios/fake/FIRMessaging.h b/messaging/src/ios/fake/FIRMessaging.h new file mode 100644 index 0000000000..802baa97d6 --- /dev/null +++ b/messaging/src/ios/fake/FIRMessaging.h @@ -0,0 +1,507 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import + +/** + * @related FIRMessaging + * + * The completion handler invoked when the registration token returns. + * If the call fails we return the appropriate `error code`, described by + * `FIRMessagingError`. + * + * @param FCMToken The valid registration token returned by FCM. + * @param error The error describing why a token request failed. The error code + * will match a value from the FIRMessagingError enumeration. + */ +typedef void(^FIRMessagingFCMTokenFetchCompletion)(NSString * _Nullable FCMToken, + NSError * _Nullable error) + NS_SWIFT_NAME(MessagingFCMTokenFetchCompletion); + + +/** + * @related FIRMessaging + * + * The completion handler invoked when the registration token deletion request is + * completed. If the call fails we return the appropriate `error code`, described + * by `FIRMessagingError`. + * + * @param error The error describing why a token deletion failed. The error code + * will match a value from the FIRMessagingError enumeration. + */ +typedef void(^FIRMessagingDeleteFCMTokenCompletion)(NSError * _Nullable error) + NS_SWIFT_NAME(MessagingDeleteFCMTokenCompletion); + +/** + * Callback to invoke once the HTTP call to FIRMessaging backend for updating + * subscription finishes. + * + * @param error The error which occurred while updating the subscription topic + * on the FIRMessaging server. This will be nil in case the operation + * was successful, or if the operation was cancelled. + */ +typedef void (^FIRMessagingTopicOperationCompletion)(NSError *_Nullable error); + +/** + * The completion handler invoked once the data connection with FIRMessaging is + * established. The data connection is used to send a continous stream of + * data and all the FIRMessaging data notifications arrive through this connection. + * Once the connection is established we invoke the callback with `nil` error. + * Correspondingly if we get an error while trying to establish a connection + * we invoke the handler with an appropriate error object and do an + * exponential backoff to try and connect again unless successful. + * + * @param error The error object if any describing why the data connection + * to FIRMessaging failed. + */ +typedef void(^FIRMessagingConnectCompletion)(NSError * __nullable error) + NS_SWIFT_NAME(MessagingConnectCompletion) + __deprecated_msg("Please listen for the FIRMessagingConnectionStateChangedNotification " + "NSNotification instead."); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + * Notification sent when the upstream message has been delivered + * successfully to the server. The notification object will be the messageID + * of the successfully delivered message. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendSuccessNotification + NS_SWIFT_NAME(MessagingSendSuccess); + +/** + * Notification sent when the upstream message was failed to be sent to the + * server. The notification object will be the messageID of the failed + * message. The userInfo dictionary will contain the relevant error + * information for the failure. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendErrorNotification + NS_SWIFT_NAME(MessagingSendError); + +/** + * Notification sent when the Firebase messaging server deletes pending + * messages due to exceeded storage limits. This may occur, for example, when + * the device cannot be reached for an extended period of time. + * + * It is recommended to retrieve any missing messages directly from the + * server. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingMessagesDeletedNotification + NS_SWIFT_NAME(MessagingMessagesDeleted); + +/** + * Notification sent when Firebase Messaging establishes or disconnects from + * an FCM socket connection. You can query the connection state in this + * notification by checking the `isDirectChannelEstablished` property of FIRMessaging. + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingConnectionStateChangedNotification + NS_SWIFT_NAME(MessagingConnectionStateChanged); + +/** + * Notification sent when the FCM registration token has been refreshed. You can also + * receive the FCM token via the FIRMessagingDelegate method + * `-messaging:didReceiveRegistrationToken:` + */ +FOUNDATION_EXPORT const NSNotificationName __nonnull + FIRMessagingRegistrationTokenRefreshedNotification + NS_SWIFT_NAME(MessagingRegistrationTokenRefreshed); +#else +/** + * Notification sent when the upstream message has been delivered + * successfully to the server. The notification object will be the messageID + * of the successfully delivered message. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendSuccessNotification + NS_SWIFT_NAME(MessagingSendSuccessNotification); + +/** + * Notification sent when the upstream message was failed to be sent to the + * server. The notification object will be the messageID of the failed + * message. The userInfo dictionary will contain the relevant error + * information for the failure. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendErrorNotification + NS_SWIFT_NAME(MessagingSendErrorNotification); + +/** + * Notification sent when the Firebase messaging server deletes pending + * messages due to exceeded storage limits. This may occur, for example, when + * the device cannot be reached for an extended period of time. + * + * It is recommended to retrieve any missing messages directly from the + * server. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingMessagesDeletedNotification + NS_SWIFT_NAME(MessagingMessagesDeletedNotification); + +/** + * Notification sent when Firebase Messaging establishes or disconnects from + * an FCM socket connection. You can query the connection state in this + * notification by checking the `isDirectChannelEstablished` property of FIRMessaging. + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingConnectionStateChangedNotification + NS_SWIFT_NAME(MessagingConnectionStateChangedNotification); + +/** + * Notification sent when the FCM registration token has been refreshed. You can also + * receive the FCM token via the FIRMessagingDelegate method + * `-messaging:didReceiveRegistrationToken:` + */ +FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingRegistrationTokenRefreshedNotification + NS_SWIFT_NAME(MessagingRegistrationTokenRefreshedNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** + * @enum FIRMessagingError + */ +typedef NS_ENUM(NSUInteger, FIRMessagingError) { + /// Unknown error. + FIRMessagingErrorUnknown = 0, + + /// FIRMessaging couldn't validate request from this client. + FIRMessagingErrorAuthentication = 1, + + /// InstanceID service cannot be accessed. + FIRMessagingErrorNoAccess = 2, + + /// Request to InstanceID backend timed out. + FIRMessagingErrorTimeout = 3, + + /// No network available to reach the servers. + FIRMessagingErrorNetwork = 4, + + /// Another similar operation in progress, bailing this one. + FIRMessagingErrorOperationInProgress = 5, + + /// Some parameters of the request were invalid. + FIRMessagingErrorInvalidRequest = 7, +} NS_SWIFT_NAME(MessagingError); + +/// Status for the downstream message received by the app. +typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) { + /// Unknown status. + FIRMessagingMessageStatusUnknown, + /// New downstream message received by the app. + FIRMessagingMessageStatusNew, +} NS_SWIFT_NAME(MessagingMessageStatus); + +/** + * The APNS token type for the app. If the token type is set to `UNKNOWN` + * Firebase Messaging will implicitly try to figure out what the actual token type + * is from the provisioning profile. + * Unless you really need to specify the type, you should use the `APNSToken` + * property instead. + */ +typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) { + /// Unknown token type. + FIRMessagingAPNSTokenTypeUnknown, + /// Sandbox token type. + FIRMessagingAPNSTokenTypeSandbox, + /// Production token type. + FIRMessagingAPNSTokenTypeProd, +} NS_SWIFT_NAME(MessagingAPNSTokenType); + +/// Information about a downstream message received by the app. +NS_SWIFT_NAME(MessagingMessageInfo) +@interface FIRMessagingMessageInfo : NSObject + +/// The status of the downstream message +@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status; + +@end + +/** + * A remote data message received by the app via FCM (not just the APNs interface). + * + * This is only for devices running iOS 10 or above. To support devices running iOS 9 or below, use + * the local and remote notifications handlers defined in UIApplicationDelegate protocol. + */ +NS_SWIFT_NAME(MessagingRemoteMessage) +@interface FIRMessagingRemoteMessage : NSObject + +/// The downstream message received by the application. +@property(nonatomic, readonly, strong, nonnull) NSDictionary *appData; +@end + +@class FIRMessaging; +/** + * A protocol to handle events from FCM for devices running iOS 10 or above. + * + * To support devices running iOS 9 or below, use the local and remote notifications handlers + * defined in UIApplicationDelegate protocol. + */ +NS_SWIFT_NAME(MessagingDelegate) +@protocol FIRMessagingDelegate + +/// This method will be called whenever FCM receives a new, default FCM token for your +/// Firebase project's Sender ID. +/// You can send this token to your application server to send notifications to this device. +- (void)messaging:(nonnull FIRMessaging *)messaging + didReceiveRegistrationToken:(nonnull NSString *)fcmToken + NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:)); + +@optional +/// This method is called on iOS 10 devices to handle data messages received via FCM through its +/// direct channel (not via APNS). For iOS 9 and below, the FCM data message is delivered via the +/// UIApplicationDelegate's -application:didReceiveRemoteNotification: method. +- (void)messaging:(nonnull FIRMessaging *)messaging + didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage + NS_SWIFT_NAME(messaging(_:didReceive:)) + __IOS_AVAILABLE(10.0); + +/// The callback to handle data message received via FCM for devices running iOS 10 or above. +- (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage + NS_SWIFT_NAME(application(received:)) + __deprecated_msg("Use FIRMessagingDelegate’s -messaging:didReceiveMessage:"); + +@end + +/** + * Firebase Messaging lets you reliably deliver messages at no cost. + * + * To send or receive messages, the app must get a + * registration token from FIRInstanceID. This token authorizes an + * app server to send messages to an app instance. + * + * In order to receive FIRMessaging messages, declare `application:didReceiveRemoteNotification:`. + */ +NS_SWIFT_NAME(Messaging) +@interface FIRMessaging : NSObject + +/** + * Delegate to handle FCM token refreshes, and remote data messages received via FCM for devices + * running iOS 10 or above. + */ +@property(nonatomic, weak, nullable) id delegate; + + +/** + * Delegate to handle remote data messages received via FCM for devices running iOS 10 or above. + */ +@property(nonatomic, weak, nullable) id remoteMessageDelegate + __deprecated_msg("Use 'delegate' property"); + +/** + * When set to YES, Firebase Messaging will automatically establish a socket-based, direct channel + * to the FCM server. You only need to enable this if you are sending upstream messages or + * receiving non-APNS, data-only messages in foregrounded apps. + * Default is NO. + */ +@property(nonatomic) BOOL shouldEstablishDirectChannel; + +/** + * Returns YES if the direct channel to the FCM server is active, NO otherwise. + */ +@property(nonatomic, readonly) BOOL isDirectChannelEstablished; + +/** + * FIRMessaging + * + * @return An instance of FIRMessaging. + */ ++ (nonnull instancetype)messaging NS_SWIFT_NAME(messaging()); + +/** + * Unavailable. Use +messaging instead. + */ +- (nonnull instancetype)init __attribute__((unavailable("Use +messaging instead."))); + +#pragma mark - APNS + +/** + * This property is used to set the APNS Token received by the application delegate. + * + * FIRMessaging uses method swizzling to ensure the APNS token is set automatically. + * However, if you have disabled swizzling by setting `FirebaseAppDelegateProxyEnabled` + * to `NO` in your app's Info.plist, you should manually set the APNS token in your + * application delegate's -application:didRegisterForRemoteNotificationsWithDeviceToken: + * method. + */ +@property(nonatomic, copy, nullable) NSData *APNSToken NS_SWIFT_NAME(apnsToken); + +#pragma mark - FCM Tokens + +/** + * The FCM token is used to identify this device so that FCM can send notifications to it. + * It is associated with your APNS token when the APNS token is supplied, so that sending + * messages to the FCM token will be delivered over APNS. + * + * The FCM token is sometimes refreshed automatically. You can be notified of these changes + * via the FIRMessagingDelegate method `-message:didReceiveRegistrationToken:`, or by + * listening for the `FIRMessagingRegistrationTokenRefreshedNotification` notification. + * + * Once you have an FCM token, you should send it to your application server, so it can use + * the FCM token to send notifications to your device. + */ +@property(nonatomic, readwrite, nullable) NSString *FCMToken NS_SWIFT_NAME(fcmToken); + +/** + * Is Firebase Messaging token auto generation enabled? If this flag is disabled, + * Firebase Messaging will not generate token automatically for message delivery. + * + * If this flag is disabled, Firebase Messaging does not generate new tokens automatically for + * message delivery. If this flag is enabled, FCM generates a registration token on application + * start when there is no existing valid token. FCM also generates a new token when an existing + * token is deleted. + * + * This setting is persisted, and is applied on future + * invocations of your application. Once explicitly set, it overrides any + * settings in your Info.plist. + * + * By default, FCM automatic initialization is enabled. If you need to change the + * default (for example, because you want to prompt the user before getting token) + * set FirebaseMessagingAutoInitEnabled to false in your application's Info.plist. + */ +@property(nonatomic, assign, getter=isAutoInitEnabled) BOOL autoInitEnabled; + +/** + * Retrieves an FCM registration token for a particular Sender ID. This registration token is + * not cached by FIRMessaging. FIRMessaging should have an APNS token set before calling this + * to ensure that notifications can be delivered via APNS using this FCM token. You may + * re-retrieve the FCM token once you have the APNS token set, to associate it with the FCM + * token. The default FCM token is automatically associated with the APNS token, if the APNS + * token data is available. + * + * @param senderID The Sender ID for a particular Firebase project. + * @param completion The completion handler to handle the token request. + */ +- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID + completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion + NS_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:)); + + +/** + * Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send + * notifications to that FCM token. + * + * @param senderID The senderID for a particular Firebase project. + * @param completion The completion handler to handle the token deletion. + */ +- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID + completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion + NS_SWIFT_NAME(deleteFCMToken(forSenderID:completion:)); + + +#pragma mark - Connect + +/** + * Create a FIRMessaging data connection which will be used to send the data notifications + * sent by your server. It will also be used to send ACKS and other messages based + * on the FIRMessaging ACKS and other messages based on the FIRMessaging protocol. + * + * + * @param handler The handler to be invoked once the connection is established. + * If the connection fails we invoke the handler with an + * appropriate error code letting you know why it failed. At + * the same time, FIRMessaging performs exponential backoff to retry + * establishing a connection and invoke the handler when successful. + */ +- (void)connectWithCompletion:(nonnull FIRMessagingConnectCompletion)handler + NS_SWIFT_NAME(connect(handler:)) + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead."); + +/** + * Disconnect the current FIRMessaging data connection. This stops any attempts to + * connect to FIRMessaging. Calling this on an already disconnected client is a no-op. + * + * Call this before `teardown` when your app is going to the background. + * Since the FIRMessaging connection won't be allowed to live when in background it is + * prudent to close the connection. + */ +- (void)disconnect + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead."); + +#pragma mark - Topics + +/** + * Asynchronously subscribes to a topic. + * + * @param topic The name of the topic, for example, @"sports". + */ +- (void)subscribeToTopic:(nonnull NSString *)topic NS_SWIFT_NAME(subscribe(toTopic:)); + +/** + * Asynchronously subscribes to a topic. + * + * @param topic The name of the topic, for example, @"sports". + * @param completion The completion that is invoked once the subscribe call ends. In case of + * success, nil error is returned. Otherwise, an appropriate error object is + * returned. + */ +- (void)subscribeToTopic:(nonnull NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion; + +/** + * Asynchronously unsubscribe from a topic. + * + * @param topic The name of the topic, for example @"sports". + */ +- (void)unsubscribeFromTopic:(nonnull NSString *)topic NS_SWIFT_NAME(unsubscribe(fromTopic:)); + +/** + * Asynchronously unsubscribe from a topic. + * + * @param topic The name of the topic, for example @"sports". + * @param completion The completion that is invoked once the subscribe call ends. In case of + * success, nil error is returned. Otherwise, an appropriate error object is + * returned. + */ +- (void)unsubscribeFromTopic:(nonnull NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion; + +#pragma mark - Upstream + +/** + * Sends an upstream ("device to cloud") message. + * + * The message is queued if we don't have an active connection. + * You can only use the upstream feature if your FCM implementation + * uses the XMPP server protocol. + * + * @param message Key/Value pairs to be sent. Values must be String, any + * other type will be ignored. + * @param to A string identifying the receiver of the message. For FCM + * project IDs the value is `SENDER_ID@gcm.googleapis.com`. + * @param messageID The ID of the message. This is generated by the application. It + * must be unique for each message generated by this application. + * It allows error callbacks and debugging, to uniquely identify + * each message. + * @param ttl The time to live for the message. In case we aren't able to + * send the message before the TTL expires we will send you a + * callback. If 0, we'll attempt to send immediately and return + * an error if we're not connected. Otherwise, the message will + * be queued. As for server-side messages, we don't return an error + * if the message has been dropped because of TTL; this can happen + * on the server side, and it would require extra communication. + */ +- (void)sendMessage:(nonnull NSDictionary *)message + to:(nonnull NSString *)receiver + withMessageID:(nonnull NSString *)messageID + timeToLive:(int64_t)ttl; + +#pragma mark - Analytics + +/** + * Use this to track message delivery and analytics for messages, typically + * when you receive a notification in `application:didReceiveRemoteNotification:`. + * However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled` + * flag to NO in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing + * or set to YES in your Info.plist, the library will call this automatically. + * + * @param message The downstream message received by the application. + * + * @return Information about the downstream message. + */ +- (nonnull FIRMessagingMessageInfo *)appDidReceiveMessage:(nonnull NSDictionary *)message; + +@end diff --git a/messaging/src/ios/fake/FIRMessaging.mm b/messaging/src/ios/fake/FIRMessaging.mm new file mode 100644 index 0000000000..9ef71450fa --- /dev/null +++ b/messaging/src/ios/fake/FIRMessaging.mm @@ -0,0 +1,125 @@ +// Copyright 2017 Google LLC +// +// 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. + +#import "messaging/src/ios/fake/FIRMessaging.h" + +#include "testing/reporter_impl.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRMessagingMessageInfo + +- (instancetype)initWithStatus:(FIRMessagingMessageStatus)status { + self = [super init]; + if (self) { + _status = status; + } + return self; +} + +@end + +@implementation FIRMessaging + +- (instancetype)initInternal { + self = [super init]; + return self; +} + ++ (instancetype)messaging { + static FIRMessaging *messaging; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Start Messaging (Fully initialize in one place). + messaging = [[FIRMessaging alloc] initInternal]; + }); + return messaging; +} + +BOOL is_auto_init_enabled = true; + +- (BOOL)isAutoInitEnabled { + return is_auto_init_enabled; +} + +- (void)setAutoInitEnabled:(BOOL)autoInitEnabled { + is_auto_init_enabled = autoInitEnabled; +} + +- (void)retrieveFCMTokenForSenderID:(NSString *)senderID + completion:(FIRMessagingFCMTokenFetchCompletion)completion + NS_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:)) {} + +- (void)deleteFCMTokenForSenderID:(NSString *)senderID + completion:(FIRMessagingDeleteFCMTokenCompletion)completion + NS_SWIFT_NAME(deleteFCMToken(forSenderID:completion:)) {} + +- (void)connectWithCompletion:(FIRMessagingConnectCompletion)handler + NS_SWIFT_NAME(connect(handler:)) + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.") {} + +- (void)disconnect + __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.") {} + ++ (NSString *)normalizeTopic:(NSString *)topic { + return topic; +} + +- (void)subscribeToTopic:(NSString *)topic NS_SWIFT_NAME(subscribe(toTopic:)) { + static const char fake[] = "-[FIRMessaging subscribeToTopic:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} + +- (void)subscribeToTopic:(NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion { + static const char fake[] = "-[FIRMessaging subscribeToTopic:completion:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} + +- (void)unsubscribeFromTopic:(NSString *)topic NS_SWIFT_NAME(unsubscribe(fromTopic:)) { + static const char fake[] = "-[FIRMessaging unsubscribeFromTopic:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} +- (void)unsubscribeFromTopic:(NSString *)topic + completion:(nullable FIRMessagingTopicOperationCompletion)completion { + static const char fake[] = "-[FIRMessaging unsubscribeFromTopic:completion:]"; + std::vector args = FakeReporter->GetFakeArgs(fake); + args.push_back(topic.UTF8String); + FakeReporter->AddReport(fake, "", args); +} + +- (void)sendMessage:(NSDictionary *)message + to:(NSString *)receiver + withMessageID:(NSString *)messageID + timeToLive:(int64_t)ttl { + FakeReporter->AddReport("-[FIRMessaging sendMessage:to:withMessageID:timeToLive:]", + { receiver.UTF8String, messageID.UTF8String, + [NSString stringWithFormat:@"%lld", ttl].UTF8String }); +} + +- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message { + FIRMessagingMessageInfo *info = + [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusUnknown]; + return info; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java b/messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java new file mode 100644 index 0000000000..f5f96392b8 --- /dev/null +++ b/messaging/src_java/fake/com/google/firebase/messaging/FirebaseMessaging.java @@ -0,0 +1,81 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import android.text.TextUtils; +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.FakeReporter; +import java.util.ArrayList; + +/** + * Fake FirebaseMessaging class. + */ +public class FirebaseMessaging { + + public static synchronized FirebaseMessaging getInstance() { + return new FirebaseMessaging(); + } + + public boolean isAutoInitEnabled() { + return autoInitEnabled_; + } + + public void setAutoInitEnabled(boolean enable) { + autoInitEnabled_ = enable; + } + + public void send(RemoteMessage message) { + FakeReporter.addReport("FirebaseMessaging.send", String.valueOf(message.to), + String.valueOf(message.data), String.valueOf(message.messageId), + String.valueOf(message.from), String.valueOf(message.ttl)); + if (TextUtils.isEmpty(message.to)) { + throw new IllegalArgumentException("Missing 'to'"); + } + } + + public Task subscribeToTopic(String topic) { + String fake = "FirebaseMessaging.subscribeToTopic"; + ArrayList args = new ArrayList<>(FakeReporter.getFakeArgs(fake)); + args.add(String.valueOf(topic)); + FakeReporter.addReport(fake, args.toArray(new String[0])); + if (TextUtils.isEmpty(topic) || "$invalid".equals(topic)) { + throw new IllegalArgumentException("Invalid topic: " + topic); + } + return Task.forResult(fake, null); + } + + public Task unsubscribeFromTopic(String topic) { + String fake = "FirebaseMessaging.unsubscribeFromTopic"; + ArrayList args = new ArrayList<>(FakeReporter.getFakeArgs(fake)); + args.add(String.valueOf(topic)); + FakeReporter.addReport(fake, args.toArray(new String[0])); + if (TextUtils.isEmpty(topic) || "$invalid".equals(topic)) { + throw new IllegalArgumentException("Invalid topic: " + topic); + } + return Task.forResult(fake, null); + } + + public void setDeliveryMetricsExportToBigQuery(boolean enable) { + deliveryMetricsExportToBigQueryEnabled = enable; + } + + public boolean deliveryMetricsExportToBigQueryEnabled() { + return deliveryMetricsExportToBigQueryEnabled; + } + + private boolean deliveryMetricsExportToBigQueryEnabled = false; + + private boolean autoInitEnabled_ = true; +} diff --git a/messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java b/messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java new file mode 100644 index 0000000000..b65cd5e9b5 --- /dev/null +++ b/messaging/src_java/fake/com/google/firebase/messaging/RemoteMessage.java @@ -0,0 +1,73 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging; + +import java.util.Map; + +/** + * Fake RemoteMessage class. + */ +public class RemoteMessage { + + public final String from; + public final String to; + public final Map data; + public final Integer ttl; + public final String messageId; + + private RemoteMessage( + String from, String to, Map data, Integer ttl, String messageId) { + this.from = from; + this.to = to; + this.data = data; + this.ttl = ttl; + this.messageId = messageId; + } + + /** + * Fake Builder class. + */ + public static class Builder { + + private final String to; + private Map data; + private Integer ttl; + private String messageId; + private String from; + + public Builder(String to) { + this.to = to; + } + + public Builder setData(Map data) { + this.data = data; + return this; + } + + public Builder setTtl(int ttl) { + this.ttl = ttl; + return this; + } + + public Builder setMessageId(String messageId) { + this.messageId = messageId; + return this; + } + + public RemoteMessage build() { + return new RemoteMessage("my_from", to, data, ttl, messageId); + } + } +} diff --git a/messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java b/messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java new file mode 100644 index 0000000000..d7a7efc6ae --- /dev/null +++ b/messaging/src_java/fake/com/google/firebase/messaging/cpp/RegistrationIntentService.java @@ -0,0 +1,21 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.messaging.cpp; + +/** + * Fake RegistrationIntentService class. + */ +public class RegistrationIntentService { +} diff --git a/messaging/tests/CMakeLists.txt b/messaging/tests/CMakeLists.txt new file mode 100644 index 0000000000..afad30765d --- /dev/null +++ b/messaging/tests/CMakeLists.txt @@ -0,0 +1,78 @@ +# Copyright 2019 Google LLC +# +# 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. + +if(ANDROID OR IOS) + set(messaging_test_util_common_SRCS + messaging_test_util.h) + set(messaging_test_util_android_SRCS + android/messaging_test_util.cc) + set(messaging_test_util_ios_SRCS + ios/messaging_test_util.mm) + + if(ANDROID) + set(messaging_test_util_SRCS + "${messaging_test_util_common_SRCS}" + "${messaging_test_util_android_SRCS}") + elseif(IOS) + set(messaging_test_util_SRCS + "${messaging_test_util_common_SRCS}" + "${messaging_test_util_ios_SRCS}") + else() + set(messaging_test_util_SRCS + "") + endif() + + add_library(firebase_messaging_test_util STATIC + ${messaging_test_util_SRCS}) + + target_include_directories(firebase_messaging_test_util + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} + ${FIREBASE_CPP_SDK_ROOT_DIR}/app/src/include + ${FIREBASE_GEN_FILE_DIR} + ) + + target_link_libraries(firebase_messaging_test_util + PRIVATE + gtest + gmock + ) + + firebase_cpp_cc_test( + firebase_messaging_test + SOURCES + messaging_test.cc + DEPENDS + firebase_app_for_testing + firebase_messaging + firebase_messaging_test_util + firebase_testing + ) + + firebase_cpp_cc_test_on_ios( + firebase_messaging_test + HOST + firebase_app_for_testing_ios + SOURCES + messaging_test.cc + DEPENDS + firebase_messaging + firebase_messaging_test_util + firebase_testing + CUSTOM_FRAMEWORKS + FirebaseMessaging + Protobuf + ) + +endif() diff --git a/messaging/tests/android/cpp/message_reader_test.cc b/messaging/tests/android/cpp/message_reader_test.cc new file mode 100644 index 0000000000..011f33a9e2 --- /dev/null +++ b/messaging/tests/android/cpp/message_reader_test.cc @@ -0,0 +1,289 @@ +// Copyright 2019 Google LLC +// +// 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 +#include +#include + +#include "app/src/util.h" +#include "messaging/messaging_generated.h" +#include "messaging/src/android/cpp/message_reader.h" +#include "messaging/src/include/firebase/messaging.h" +#include "gtest/gtest.h" + +// Since we're compiling a subset of the Android library on all platforms, +// we need to register a stub module initializer referenced by messaging.h +// to satisfy the linker. +FIREBASE_APP_REGISTER_CALLBACKS(messaging, { return kInitResultSuccess; }, {}); + +namespace firebase { +namespace messaging { +namespace internal { + +using com::google::firebase::messaging::cpp::CreateDataPairDirect; +using com::google::firebase::messaging::cpp::CreateSerializedEvent; +using com::google::firebase::messaging::cpp::CreateSerializedMessageDirect; +using com::google::firebase::messaging::cpp::CreateSerializedNotificationDirect; +using com::google::firebase::messaging::cpp::CreateSerializedTokenReceived; +using com::google::firebase::messaging::cpp::DataPair; +using com::google::firebase::messaging::cpp::FinishSerializedEventBuffer; +using com::google::firebase::messaging::cpp::SerializedEventUnion; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedMessage; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedTokenReceived; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_MAX; +using flatbuffers::FlatBufferBuilder; + +class MessageReaderTest : public ::testing::Test { + protected: + void SetUp() override {} + + void TearDown() override { + messages_received_.clear(); + tokens_received_.clear(); + } + + // Stores the message in this class. + static void MessageReceived(const Message& message, void* callback_data) { + MessageReaderTest* test = + reinterpret_cast(callback_data); + test->messages_received_.push_back(message); + } + + // Stores the token in this class. + static void TokenReceived(const char* token, void* callback_data) { + MessageReaderTest* test = + reinterpret_cast(callback_data); + test->tokens_received_.push_back(std::string(token)); + } + + protected: + // Messages received by MessageReceived(). + std::vector messages_received_; + // Tokens received by TokenReceived(). + std::vector tokens_received_; +}; + +TEST_F(MessageReaderTest, Construct) { + MessageReader reader( + MessageReaderTest::MessageReceived, reinterpret_cast(1), + MessageReaderTest::TokenReceived, reinterpret_cast(2)); + EXPECT_EQ(reinterpret_cast(MessageReaderTest::MessageReceived), + reinterpret_cast(reader.message_callback())); + EXPECT_EQ(reinterpret_cast(1), reader.message_callback_data()); + EXPECT_EQ(reinterpret_cast(MessageReaderTest::TokenReceived), + reinterpret_cast(reader.token_callback())); + EXPECT_EQ(reinterpret_cast(2), reader.token_callback_data()); +} + +// Read an empty buffer and ensure no data is parsed. +TEST_F(MessageReaderTest, ReadFromBufferEmpty) { + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(std::string()); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Read from a buffer that is too small and ensure no data is parsed. +TEST_F(MessageReaderTest, ReadFromBufferTooSmall) { + std::string buffer; + buffer.push_back('b'); + buffer.push_back('d'); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Read from a buffer with a header length that overflows the buffer size. +TEST_F(MessageReaderTest, ReadFromBufferHeaderOverflow) { + int32_t header = 9; + std::string buffer; + buffer.resize(sizeof(header)); + memcpy(&buffer[0], reinterpret_cast(&header), sizeof(header)); + buffer.push_back('5'); + buffer.push_back('6'); + buffer.push_back('7'); + buffer.push_back('8'); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Append a FlatBuffer to a string with the size of the FlatBuffer stored +// in a 32-bit integer header. +void AppendFlatBufferToString(std::string* output, + const FlatBufferBuilder& fbb) { + int32_t flatbuffer_size = static_cast(fbb.GetSize()); + size_t buffer_offset = output->size(); + output->resize(buffer_offset + sizeof(flatbuffer_size) + flatbuffer_size); + *(reinterpret_cast(&((*output)[buffer_offset]))) = flatbuffer_size; + memcpy(&((*output)[buffer_offset + sizeof(flatbuffer_size)]), + fbb.GetBufferPointer(), flatbuffer_size); +} + +// Read tokens from a buffer. +TEST_F(MessageReaderTest, ReadFromBufferTokenReceived) { + std::string buffer; + std::string tokens[3]; + tokens[0] = "token1"; + tokens[1] = "token2"; + tokens[2] = "token3"; + for (size_t i = 0; i < sizeof(tokens) / sizeof(tokens[0]); ++i) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedTokenReceived, + CreateSerializedTokenReceived(fbb, fbb.CreateString(tokens[i])) + .Union())); + AppendFlatBufferToString(&buffer, fbb); + } + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(3, tokens_received_.size()); + EXPECT_EQ(tokens[0], tokens_received_[0]); + EXPECT_EQ(tokens[1], tokens_received_[1]); + EXPECT_EQ(tokens[2], tokens_received_[2]); +} + +// Read a message from a buffer. +TEST_F(MessageReaderTest, ReadFromBufferMessageReceived) { + FlatBufferBuilder fbb; + std::vector> data; + data.push_back(CreateDataPairDirect(fbb, "foo", "bar")); + data.push_back(CreateDataPairDirect(fbb, "bosh", "bash")); + std::vector> body_loc_args; + body_loc_args.push_back(fbb.CreateString("1")); + body_loc_args.push_back(fbb.CreateString("2")); + std::vector> title_loc_args; + title_loc_args.push_back(fbb.CreateString("3")); + title_loc_args.push_back(fbb.CreateString("4")); + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedMessage, + CreateSerializedMessageDirect( + fbb, "from:bob", "to:jane", "collapsekey", &data, "rawdata", + "message_id", "message_type", + "high", // priority + 10, // TTL + "error0", "an error description", + CreateSerializedNotificationDirect( + fbb, "title", "body", "icon", "sound", "badge", "tag", + "color", "click_action", "body_loc_key", &body_loc_args, + "title_loc_key", &title_loc_args, "android_channel_id"), + true, // opened + "http://alink.com", + 1234, // sent time + "normal" /* original_priority */) + .Union())); + std::string buffer; + AppendFlatBufferToString(&buffer, fbb); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(1, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); + Message& message = messages_received_[0]; + EXPECT_EQ("from:bob", message.from); + EXPECT_EQ("to:jane", message.to); + EXPECT_EQ("collapsekey", message.collapse_key); + EXPECT_EQ("bar", message.data["foo"]); + EXPECT_EQ("bash", message.data["bosh"]); + EXPECT_EQ(1234, message.sent_time); + EXPECT_EQ("high", message.priority); + EXPECT_EQ("normal", message.original_priority); + EXPECT_EQ(10, message.time_to_live); + EXPECT_EQ("error0", message.error); + EXPECT_EQ("an error description", message.error_description); + EXPECT_EQ(true, message.notification_opened); + EXPECT_EQ("http://alink.com", message.link); + Notification* notification = message.notification; + EXPECT_NE(nullptr, notification); + EXPECT_EQ("title", notification->title); + EXPECT_EQ("body", notification->body); + EXPECT_EQ("icon", notification->icon); + EXPECT_EQ("sound", notification->sound); + EXPECT_EQ("click_action", notification->click_action); + EXPECT_EQ("body_loc_key", notification->body_loc_key); + EXPECT_EQ(2, notification->body_loc_args.size()); + EXPECT_EQ("1", notification->body_loc_args[0]); + EXPECT_EQ("2", notification->body_loc_args[1]); + EXPECT_EQ("title_loc_key", notification->title_loc_key); + EXPECT_EQ(2, notification->title_loc_args.size()); + EXPECT_EQ("3", notification->title_loc_args[0]); + EXPECT_EQ("4", notification->title_loc_args[1]); + AndroidNotificationParams* android = message.notification->android; + EXPECT_NE(nullptr, android); + EXPECT_EQ("android_channel_id", android->channel_id); +} + +// Try to read from a buffer with a corrupt flatbuffer +TEST_F(MessageReaderTest, ReadFromBufferCorruptFlatbuffer) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedTokenReceived, + CreateSerializedTokenReceived(fbb, fbb.CreateString("clobberme")) + .Union())); + std::string buffer; + AppendFlatBufferToString(&buffer, fbb); + for (size_t i = 0; i < fbb.GetSize(); ++i) { + buffer[sizeof(int32_t) + i] = 0xef; + } + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +// Try reading from a buffer with an invalid event type. +TEST_F(MessageReaderTest, ReadFromBufferInvalidEventType) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, + CreateSerializedEvent( + fbb, + static_cast( + SerializedEventUnion_MAX + 1), + CreateSerializedTokenReceived(fbb, fbb.CreateString("ignoreme")) + .Union())); + std::string buffer; + AppendFlatBufferToString(&buffer, fbb); + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); +} + +} // namespace internal +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/android/cpp/messaging_test_util.cc b/messaging/tests/android/cpp/messaging_test_util.cc new file mode 100644 index 0000000000..22af5f12f2 --- /dev/null +++ b/messaging/tests/android/cpp/messaging_test_util.cc @@ -0,0 +1,277 @@ +// Copyright 2017 Google LLC +// +// 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 "messaging/tests/messaging_test_util.h" + +#include +#include +#include +#include + +#include "app/src/util.h" +#include "app/src/util_android.h" +#include "messaging/messaging_generated.h" +#include "messaging/src/android/cpp/messaging_internal.h" +#include "messaging/src/include/firebase/messaging.h" +#include "testing/run_all_tests.h" +#include "flatbuffers/util.h" + +using ::com::google::firebase::messaging::cpp::CreateDataPair; +using ::com::google::firebase::messaging::cpp::CreateSerializedEvent; +using ::com::google::firebase::messaging::cpp::CreateSerializedTokenReceived; +using ::com::google::firebase::messaging::cpp::DataPair; +using ::com::google::firebase::messaging::cpp::SerializedEventUnion; +using ::com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedMessage; +using ::com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedTokenReceived; +using ::com::google::firebase::messaging::cpp::SerializedMessageBuilder; +using ::com::google::firebase::messaging::cpp::SerializedNotification; +using ::com::google::firebase::messaging::cpp::SerializedNotificationBuilder; + +namespace firebase { +namespace messaging { + +static std::string* g_local_storage_file_path; +static std::string* g_lockfile_path; + +// Lock the file referenced by g_lockfile_path. +class TestMessageLockFileLocker : private FileLocker { + public: + TestMessageLockFileLocker() : FileLocker(g_lockfile_path->c_str()) {} + ~TestMessageLockFileLocker() {} +}; + +void InitializeMessagingTest() { + JNIEnv* env = firebase::testing::cppsdk::GetTestJniEnv(); + jobject activity = firebase::testing::cppsdk::GetTestActivity(); + jobject file = env->CallObjectMethod( + activity, util::context::GetMethodId(util::context::kGetFilesDir)); + assert(env->ExceptionCheck() == false); + jstring path_jstring = reinterpret_cast(env->CallObjectMethod( + file, util::file::GetMethodId(util::file::kGetPath))); + assert(env->ExceptionCheck() == false); + std::string local_storage_dir = util::JniStringToString(env, path_jstring); + env->DeleteLocalRef(file); + g_lockfile_path = new std::string(local_storage_dir + "/" + kLockfile); + g_local_storage_file_path = + new std::string(local_storage_dir + "/" + kStorageFile); +} + +void TerminateMessagingTest() { + delete g_lockfile_path; + g_lockfile_path = nullptr; + delete g_local_storage_file_path; + g_local_storage_file_path = nullptr; +} + +static void WriteBuffer(const ::flatbuffers::FlatBufferBuilder& builder) { + TestMessageLockFileLocker file_lock; + FILE* data_file = fopen(g_local_storage_file_path->c_str(), "a"); + int size = builder.GetSize(); + fwrite(&size, sizeof(size), 1, data_file); + fwrite(builder.GetBufferPointer(), size, 1, data_file); + fclose(data_file); +} + +void OnTokenReceived(const char* tokenstr) { + flatbuffers::FlatBufferBuilder builder; + auto token = builder.CreateString(tokenstr); + auto tokenreceived = CreateSerializedTokenReceived(builder, token); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedTokenReceived, + tokenreceived.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnDeletedMessages() { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(""); + auto message_id = builder.CreateString(""); + auto message_type = builder.CreateString("deleted_messages"); + auto error = builder.CreateString(""); + auto link = builder.CreateString(""); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_message_id(message_id); + message_builder.add_message_type(message_type); + message_builder.add_error(error); + message_builder.add_notification_opened(false); + message_builder.add_link(link); + auto message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnMessageReceived(const Message& message) { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(message.from); + auto to = builder.CreateString(message.to); + auto message_id = builder.CreateString(message.message_id); + auto message_type = builder.CreateString(message.message_type); + auto error = builder.CreateString(message.error); + auto priority = builder.CreateString(message.priority); + auto original_priority = builder.CreateString(message.original_priority); + auto collapse_key = builder.CreateString(message.collapse_key); + + std::vector> data_pair_vector; + for (auto const& entry : message.data) { + auto key = builder.CreateString(entry.first); + auto value = builder.CreateString(entry.second); + auto data_pair = CreateDataPair(builder, key, value); + data_pair_vector.push_back(data_pair); + } + auto data = builder.CreateVector(data_pair_vector); + ::flatbuffers::Offset notification; + if (message.notification) { + auto title = builder.CreateString(message.notification->title); + auto body = builder.CreateString(message.notification->body); + auto icon = builder.CreateString(message.notification->icon); + auto sound = builder.CreateString(message.notification->sound); + auto badge = builder.CreateString(message.notification->badge); + auto tag = builder.CreateString(message.notification->tag); + auto color = builder.CreateString(message.notification->color); + auto click_action = + builder.CreateString(message.notification->click_action); + auto body_localization_key = + builder.CreateString(message.notification->body_loc_key); + + std::vector> + body_localization_args_vector; + for (auto const& value : message.notification->body_loc_args) { + auto body_localization_item = builder.CreateString(value); + body_localization_args_vector.push_back(body_localization_item); + } + auto body_localization_args = + builder.CreateVector(body_localization_args_vector); + + auto title_localization_key = + builder.CreateString(message.notification->title_loc_key); + + std::vector> + title_localization_args_vector; + for (auto const& value : message.notification->title_loc_args) { + auto title_localization_item = builder.CreateString(value); + title_localization_args_vector.push_back(title_localization_item); + } + auto title_localization_args = + builder.CreateVector(title_localization_args_vector); + auto android_channel_id = + message.notification->android + ? builder.CreateString(message.notification->android->channel_id) + : 0; + + SerializedNotificationBuilder notification_builder(builder); + notification_builder.add_title(title); + notification_builder.add_body(body); + notification_builder.add_icon(icon); + notification_builder.add_sound(sound); + notification_builder.add_badge(badge); + notification_builder.add_tag(tag); + notification_builder.add_color(color); + notification_builder.add_click_action(click_action); + notification_builder.add_body_loc_key(body_localization_key); + notification_builder.add_body_loc_args(body_localization_args); + notification_builder.add_title_loc_key(title_localization_key); + notification_builder.add_title_loc_args(title_localization_args); + if (message.notification->android) { + notification_builder.add_android_channel_id(android_channel_id); + } + notification = notification_builder.Finish(); + } + auto link = builder.CreateString(message.link); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_to(to); + message_builder.add_message_id(message_id); + message_builder.add_message_type(message_type); + message_builder.add_priority(priority); + message_builder.add_original_priority(original_priority); + message_builder.add_sent_time(message.sent_time); + message_builder.add_time_to_live(message.time_to_live); + message_builder.add_collapse_key(collapse_key); + if (!notification.IsNull()) { + message_builder.add_notification(notification); + } + message_builder.add_error(error); + message_builder.add_notification_opened(message.notification_opened); + message_builder.add_link(link); + message_builder.add_data(data); + auto serialized_message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + serialized_message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnMessageSent(const char* message_id) { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(""); + auto message_id_offset = builder.CreateString(message_id); + auto message_type = builder.CreateString("send_event"); + auto error = builder.CreateString(""); + auto link = builder.CreateString(""); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_message_id(message_id_offset); + message_builder.add_message_type(message_type); + message_builder.add_error(error); + message_builder.add_notification_opened(false); + message_builder.add_link(link); + auto message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnMessageSentError(const char* message_id, const char* error) { + ::flatbuffers::FlatBufferBuilder builder; + auto from = builder.CreateString(""); + auto message_id_offset = builder.CreateString(message_id); + auto message_type = builder.CreateString("send_error"); + auto error_offset = builder.CreateString(error); + auto link = builder.CreateString(""); + SerializedMessageBuilder message_builder(builder); + message_builder.add_from(from); + message_builder.add_message_id(message_id_offset); + message_builder.add_message_type(message_type); + message_builder.add_error(error_offset); + message_builder.add_notification_opened(false); + message_builder.add_link(link); + auto message = message_builder.Finish(); + auto event = CreateSerializedEvent( + builder, + SerializedEventUnion_SerializedMessage, + message.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void SleepMessagingTest(double seconds) { + sleep(static_cast(seconds + 0.5)); +} + +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/ios/messaging_test_util.mm b/messaging/tests/ios/messaging_test_util.mm new file mode 100644 index 0000000000..106381b769 --- /dev/null +++ b/messaging/tests/ios/messaging_test_util.mm @@ -0,0 +1,99 @@ +// Copyright 2017 Google LLC +// +// 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 "messaging/tests/messaging_test_util.h" + +#import + +#include "app/src/log.h" +#include "messaging/src/include/firebase/messaging.h" + +#import "messaging/src/ios/fake/FIRMessaging.h" + +namespace firebase { +namespace messaging { + +// Message keys. +static NSString *const kFrom = @"from"; +static NSString *const kTo = @"to"; +static NSString *const kCollapseKey = @"collapse_key"; +static NSString *const kMessageID = @"gcm.message_id"; +static NSString *const kMessageType = @"message_type"; +static NSString *const kPriority = @"priority"; +static NSString *const kTimeToLive = @"time_to_live"; +static NSString *const kError = @"error"; +static NSString *const kErrorDescription = @"error_description"; + +// Notification keys. +static NSString *const kTitle = @"title"; +static NSString *const kBody = @"body"; +static NSString *const kSound = @"sound"; +static NSString *const kBadge = @"badge"; + +// Dual purpose body text or data dictionary. +static NSString *const kAlert = @"alert"; + + +void InitializeMessagingTest() {} + +void TerminateMessagingTest() { + [FIRMessaging messaging].FCMToken = nil; +} + +void OnTokenReceived(const char* tokenstr) { + [FIRMessaging messaging].FCMToken = @(tokenstr); + [[FIRMessaging messaging].delegate messaging:[FIRMessaging messaging] + didReceiveRegistrationToken:@(tokenstr)]; +} + +void SleepMessagingTest(double seconds) { + // We want the main loop to process messages while we wait. + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:seconds]]; +} + +void OnMessageReceived(const Message& message) { + NSMutableDictionary* userData = [NSMutableDictionary dictionary]; + userData[kMessageID] = @(message.message_id.c_str()); + userData[kTo] = @(message.to.c_str()); + userData[kFrom] = @(message.from.c_str()); + userData[kCollapseKey] = @(message.collapse_key.c_str()); + userData[kMessageType] = @(message.message_type.c_str()); + userData[kPriority] = @(message.priority.c_str()); + userData[kTimeToLive] = @(message.time_to_live); + userData[kError] = @(message.error.c_str()); + userData[kErrorDescription] = @(message.error_description.c_str()); + for (const auto& entry : message.data) { + userData[@(entry.first.c_str())] = @(entry.second.c_str()); + } + + if (message.notification) { + NSMutableDictionary* alert = [NSMutableDictionary dictionary]; + alert[kTitle] = @(message.notification->title.c_str()); + alert[kBody] = @(message.notification->body.c_str()); + NSMutableDictionary* aps = [NSMutableDictionary dictionary]; + aps[kSound] = @(message.notification->sound.c_str()); + aps[kBadge] = @(message.notification->badge.c_str()); + aps[kAlert] = alert; + userData[@"aps"] = aps; + } + [[[UIApplication sharedApplication] delegate] application:[UIApplication sharedApplication] + didReceiveRemoteNotification:userData]; +} + +void OnMessageSent(const char* message_id) {} + +void OnMessageSentError(const char* message_id, const char* error) {} + +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/messaging_test.cc b/messaging/tests/messaging_test.cc new file mode 100644 index 0000000000..3da4ed9df4 --- /dev/null +++ b/messaging/tests/messaging_test.cc @@ -0,0 +1,380 @@ +// Copyright 2017 Google LLC +// +// 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. + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "messaging/src/include/firebase/messaging.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#if defined(__APPLE__) +#include +#endif // defined(__APPLE_) + +#include "app/src/util.h" +#include "messaging/tests/messaging_test_util.h" +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using ::testing::StrEq; + +namespace firebase { +namespace messaging { + +class MessagingTestListener : public Listener { + public: + void OnMessage(const Message& message) override; + void OnTokenReceived(const char* token) override; + + const Message& GetMessage() const { + return message_; + } + + const std::string& GetToken() const { + return token_; + } + + int GetOnTokenReceivedCount() const { + return on_token_received_count_; + } + + int GetOnMessageReceivedCount() const { + return on_message_received_count_; + } + + private: + Message message_; + std::string token_; + int on_token_received_count_ = 0; + int on_message_received_count_ = 0; +}; + +class MessagingTest : public ::testing::Test { + protected: + void SetUp() override { + // Cache the local storage file and lockfile. + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + reporter_.reset(); + + firebase_app_ = testing::CreateApp(); + InitializeMessagingTest(); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + } + + void TearDown() override { + firebase::testing::cppsdk::ConfigReset(); + Terminate(); + TerminateMessagingTest(); + delete firebase_app_; + firebase_app_ = nullptr; + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + void AddExpectationAndroid( + const char* fake, std::initializer_list args) { + reporter_.addExpectation( + fake, "", firebase::testing::cppsdk::kAndroid, args); + } + + void AddExpectationApple( + const char* fake, std::initializer_list args) { + reporter_.addExpectation(fake, "", firebase::testing::cppsdk::kIos, args); + } + + App* firebase_app_ = nullptr; + MessagingTestListener listener_; + firebase::testing::cppsdk::Reporter reporter_; +}; + +void MessagingTestListener::OnMessage(const Message& message) { + message_ = message; + on_message_received_count_++; +} + +void MessagingTestListener::OnTokenReceived(const char* token) { + token_ = token; + on_token_received_count_++; +} + +// Tests only run on Android for now. +TEST_F(MessagingTest, TestInitializeTwice) { + MessagingTestListener listener; + EXPECT_EQ(Initialize(*firebase_app_, &listener), kInitResultSuccess); +} + +// The order of these matter because of the global flag +// g_registration_token_received +TEST_F(MessagingTest, TestSubscribeNoRegistration) { + Subscribe("topic"); + SleepMessagingTest(1); + // Android should cache the call, iOS will subscribe right away. + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"topic"}); +} + +// TODO(westarle): break up this test when subscriber queuing is testable. +TEST_F(MessagingTest, TestSubscribeBeforeRegistration) { + Subscribe("$invalid"); + Subscribe("subscribe_topic1"); + Subscribe("subscribe_topic2"); + Unsubscribe("$invalid"); + Unsubscribe("unsubscribe_topic1"); + Unsubscribe("unsubscribe_topic2"); + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"$invalid", "subscribe_topic1", "subscribe_topic2"}); + AddExpectationApple("-[FIRMessaging unsubscribeFromTopic:completion:]", + {"$invalid", "unsubscribe_topic1", "unsubscribe_topic2"}); + + // No requests to Java API yet, iOS should go ahead and forward. + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + + OnTokenReceived("my_token"); + SleepMessagingTest(1); + AddExpectationAndroid("FirebaseMessaging.subscribeToTopic", + {"$invalid", "subscribe_topic1", "subscribe_topic2"}); + + AddExpectationAndroid("FirebaseMessaging.unsubscribeFromTopic", + {"$invalid", "unsubscribe_topic1", "unsubscribe_topic2"}); +} + +TEST_F(MessagingTest, TestSubscribeAfterRegistration) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Subscribe("topic"); + + AddExpectationAndroid("FirebaseMessaging.subscribeToTopic", {"topic"}); + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"topic"}); +} + +TEST_F(MessagingTest, TestUnsubscribeAfterRegistration) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Unsubscribe("topic"); + AddExpectationAndroid("FirebaseMessaging.unsubscribeFromTopic", {"topic"}); + AddExpectationApple("-[FIRMessaging unsubscribeFromTopic:completion:]", + {"topic"}); +} + +TEST_F(MessagingTest, TestTokenReceived) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); +} + +TEST_F(MessagingTest, TestTokenReceivedBeforeInitialize) { + Terminate(); + OnTokenReceived("my_token"); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); +} + +TEST_F(MessagingTest, TestTwoTokensReceivedBeforeInitialize) { + Terminate(); + OnTokenReceived("my_token1"); + OnTokenReceived("my_token2"); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token2")); +} + +TEST_F(MessagingTest, TestTwoTokensReceivedAfterInitialize) { + OnTokenReceived("my_token1"); + OnTokenReceived("my_token2"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token2")); + EXPECT_EQ(listener_.GetOnTokenReceivedCount(), 2); +} + +TEST_F(MessagingTest, TestTwoIdenticalTokensReceived) { + OnTokenReceived("my_token"); + OnTokenReceived("my_token"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); + EXPECT_EQ(listener_.GetOnTokenReceivedCount(), 1); +} + +TEST_F(MessagingTest, TestTokenReceivedNoListener) { + Terminate(); + EXPECT_EQ(Initialize(*firebase_app_, nullptr), kInitResultSuccess); + OnTokenReceived("my_token"); + SleepMessagingTest(1); + SetListener(&listener_); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetToken(), StrEq("my_token")); + EXPECT_EQ(listener_.GetOnTokenReceivedCount(), 1); +} + +TEST_F(MessagingTest, TestSubscribeInvalidTopic) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Subscribe("$invalid"); + AddExpectationAndroid("FirebaseMessaging.subscribeToTopic", {"$invalid"}); + AddExpectationApple("-[FIRMessaging subscribeToTopic:completion:]", + {"$invalid"}); +} + +TEST_F(MessagingTest, TestUnsubscribeInvalidTopic) { + OnTokenReceived("my_token"); + SleepMessagingTest(1); + Unsubscribe("$invalid"); + AddExpectationAndroid("FirebaseMessaging.unsubscribeFromTopic", {"$invalid"}); + AddExpectationApple("-[FIRMessaging unsubscribeFromTopic:completion:]", + {"$invalid"}); +} + +TEST_F(MessagingTest, TestDataMessageReceived) { + Message message; + message.from = "my_from"; + message.data["my_key"] = "my_value"; + OnMessageReceived(message); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().from, StrEq("my_from")); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("")); + EXPECT_THAT(listener_.GetMessage().error, StrEq("")); + EXPECT_THAT(listener_.GetMessage().data.at("my_key"), StrEq("my_value")); +} + +TEST_F(MessagingTest, TestNotificationReceived) { + Message send_message; + send_message.from = "my_from"; + send_message.to = "my_to"; + send_message.message_id = "id"; + send_message.message_type = "type"; + send_message.error = ""; + send_message.data["my_key"] = "my_value"; + send_message.notification = new Notification; + send_message.notification->title = "my_title"; + send_message.notification->body = "my_body"; + send_message.notification->icon = "my_icon"; + send_message.notification->sound = "my_sound"; + send_message.notification->tag = "my_tag"; + send_message.notification->color = "my_color"; + send_message.notification->click_action = "my_click_action"; + send_message.notification->body_loc_key = "my_body_localization_key"; + send_message.notification->body_loc_args.push_back( + "my_body_localization_item"); + send_message.notification->title_loc_key = "my_title_localization_key"; + send_message.notification->title_loc_args.push_back( + "my_title_localization_item"); + send_message.notification_opened = true; + send_message.notification->android = new AndroidNotificationParams; + send_message.notification->android->channel_id = "my_android_channel_id"; + send_message.collapse_key = "my_collapse_key"; + send_message.priority = "my_priority"; + send_message.original_priority = "normal"; + send_message.time_to_live = 1234; + send_message.sent_time = 5678; + OnMessageReceived(send_message); + SleepMessagingTest(1); + const Message& message = listener_.GetMessage(); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(message.from, StrEq("my_from")); + EXPECT_THAT(message.to, StrEq("my_to")); + EXPECT_THAT(message.message_id, StrEq("id")); + EXPECT_THAT(message.message_type, StrEq("type")); + EXPECT_THAT(message.error, StrEq("")); + EXPECT_THAT(message.data.at("my_key"), StrEq("my_value")); + EXPECT_TRUE(message.notification_opened); + EXPECT_THAT(message.notification->title, StrEq("my_title")); + EXPECT_THAT(message.notification->body, StrEq("my_body")); + EXPECT_THAT(message.notification->sound, StrEq("my_sound")); + EXPECT_THAT(message.collapse_key, StrEq("my_collapse_key")); + EXPECT_THAT(message.priority, StrEq("my_priority")); + EXPECT_EQ(message.time_to_live, 1234); +#if !TARGET_OS_IPHONE + EXPECT_THAT(message.original_priority, StrEq("normal")); + EXPECT_EQ(message.sent_time, 5678); +#endif // !TARGET_OS_IPHONE + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_THAT(message.notification->icon, StrEq("my_icon")); + EXPECT_THAT(message.notification->tag, StrEq("my_tag")); + EXPECT_THAT(message.notification->color, StrEq("my_color")); + EXPECT_THAT(message.notification->click_action, StrEq("my_click_action")); + EXPECT_THAT( + message.notification->body_loc_key, StrEq("my_body_localization_key")); + EXPECT_THAT(message.notification->body_loc_args[0], + StrEq("my_body_localization_item")); + EXPECT_THAT( + message.notification->title_loc_key, StrEq("my_title_localization_key")); + EXPECT_THAT(message.notification->title_loc_args[0], + StrEq("my_title_localization_item")); + EXPECT_THAT(message.notification->android->channel_id, + StrEq("my_android_channel_id")); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + +TEST_F(MessagingTest, TestOnDeletedMessages) { + OnDeletedMessages(); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().from, StrEq("")); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("deleted_messages")); + EXPECT_THAT(listener_.GetMessage().error, StrEq("")); +} + +TEST_F(MessagingTest, TestSendMessage) { + Message message; + message.to = "my_to"; + message.message_id = "my_message_id"; + message.data["my_key"] = "my_value"; + message.time_to_live = 1000; + Send(message); + AddExpectationAndroid("FirebaseMessaging.send", + {"my_to", "{my_key=my_value}", "my_message_id", "my_from", "1000"}); +} + +TEST_F(MessagingTest, TestOnMessageSent) { + OnMessageSent("my_message_id"); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("my_message_id")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("send_event")); +} + +TEST_F(MessagingTest, TestOnSendError) { + OnMessageSentError("my_message_id", "my_exception"); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnMessageReceivedCount(), 1); + EXPECT_THAT(listener_.GetMessage().message_id, StrEq("my_message_id")); + EXPECT_THAT(listener_.GetMessage().message_type, StrEq("send_error")); + EXPECT_THAT(listener_.GetMessage().error, StrEq("my_exception")); +} + + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +} // namespace messaging +} // namespace firebase diff --git a/messaging/tests/messaging_test_util.h b/messaging/tests/messaging_test_util.h new file mode 100644 index 0000000000..b027cd8ef7 --- /dev/null +++ b/messaging/tests/messaging_test_util.h @@ -0,0 +1,51 @@ +// Copyright 2017 Google LLC +// +// 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. + +// This file contains utility methods used by messaging tests where the +// implementation diverges across platforms. +#ifndef FIREBASE_MESSAGING_CLIENT_CPP_TESTS_MESSAGING_TEST_UTIL_H_ +#define FIREBASE_MESSAGING_CLIENT_CPP_TESTS_MESSAGING_TEST_UTIL_H_ + +namespace firebase { +namespace messaging { + +struct Message; + +// Sleep this thread for some amount of time and process important messages. +// e.g. let the Android messaging implementation wake up the thread watching +// the file. +void SleepMessagingTest(double seconds); + +// Once-per-test platform specific initialization (e.g. the Android test +// implementation will initialize filenames by JNI calls. +void InitializeMessagingTest(); + +// Once-per-test platform-specific teardown. +void TerminateMessagingTest(); + +// Simulate a token received/refresh event from the OS-level implementation. +void OnTokenReceived(const char* tokenstr); + +void OnDeletedMessages(); + +void OnMessageReceived(const Message& message); + +void OnMessageSent(const char* message_id); + +void OnMessageSentError(const char* message_id, const char* error); + +} // namespace messaging +} // namespace firebase + +#endif // FIREBASE_MESSAGING_CLIENT_CPP_TESTS_MESSAGING_TEST_UTIL_H_ diff --git a/remote_config/src/desktop/rest_fake.cc b/remote_config/src/desktop/rest_fake.cc new file mode 100644 index 0000000000..addb8a227f --- /dev/null +++ b/remote_config/src/desktop/rest_fake.cc @@ -0,0 +1,73 @@ +// Copyright 2017 Google LLC +// +// 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 "remote_config/src/desktop/rest.h" + +#include // NOLINT +#include +#include + +#include "firebase/app.h" +#include "remote_config/src/desktop/config_data.h" +#include "remote_config/src/desktop/rest_nanopb_encode.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +// Stub REST implementation. +// The purpose of this class is to hold content and not actually do anything +// with it when the normal API calls happen. +RemoteConfigREST::RemoteConfigREST(const firebase::AppOptions& app_options, + const LayeredConfigs& configs, + uint64_t cache_expiration_in_seconds) + : app_package_name_(app_options.app_id()), + app_gmp_project_id_(app_options.project_id()), + configs_(configs), + cache_expiration_in_seconds_(cache_expiration_in_seconds), + fetch_future_sem_(0) { + configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "value"}}}}), 1000000); + + configs_.metadata.set_info( + ConfigInfo{0, kLastFetchStatusSuccess, kFetchFailureReasonError, 0}); + configs_.metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace", "digest"}})); +} + +RemoteConfigREST::~RemoteConfigREST() {} + +void RemoteConfigREST::Fetch(const App& app) {} + +void RemoteConfigREST::SetupRestRequest() {} + +ConfigFetchRequest RemoteConfigREST::GetFetchRequestData() { + return ConfigFetchRequest(); +} + +void RemoteConfigREST::GetPackageData(PackageData* package_data) {} + +void RemoteConfigREST::ParseRestResponse() {} + +void RemoteConfigREST::ParseProtoResponse(const std::string& proto_str) {} + +void RemoteConfigREST::FetchSuccess(LastFetchStatus status) {} + +void RemoteConfigREST::FetchFailure(FetchFailureReason reason) {} + +uint64_t RemoteConfigREST::MillisecondsSinceEpoch() { return 0; } + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java new file mode 100644 index 0000000000..9e25f0857b --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -0,0 +1,269 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.android.gms.tasks.Task; +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; +import com.google.firebase.testing.cppsdk.TickerAndroid; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** Fake FirebaseRemoteConfig */ +public class FirebaseRemoteConfig { + + private static final String FN_ACTIVATE_FETCHED = "FirebaseRemoteConfig.activateFetched"; + private static final String FN_SET_DEFAULTS = "FirebaseRemoteConfig.setDefaults"; + private static final String FN_SET_CONFIG_SETTINGS = "FirebaseRemoteConfig.setConfigSettings"; + private static final String FN_GET_LONG = "FirebaseRemoteConfig.getLong"; + private static final String FN_GET_BYTE_ARRAY = "FirebaseRemoteConfig.getByteArray"; + private static final String FN_GET_STRING = "FirebaseRemoteConfig.getString"; + private static final String FN_GET_BOOLEAN = "FirebaseRemoteConfig.getBoolean"; + private static final String FN_GET_DOUBLE = "FirebaseRemoteConfig.getDouble"; + private static final String FN_GET_VALUE = "FirebaseRemoteConfig.getValue"; + private static final String FN_GET_INFO = "FirebaseRemoteConfig.getInfo"; + private static final String FN_GET_KEYS_BY_PREFIX = "FirebaseRemoteConfig.getKeysByPrefix"; + private static final String FN_GET_ALL = "FirebaseRemoteConfig.getAll"; + private static final String FN_FETCH = "FirebaseRemoteConfig.fetch"; + private static final String FN_ENSURE_INITIALIZED = "FirebaseRemoteConfig.ensureInitialized"; + private static final String FN_ACTIVATE = "FirebaseRemoteConfig.activate"; + private static final String FN_FETCH_AND_ACTIVATE = "FirebaseRemoteConfig.fetchAndActivate"; + private static final String FN_SET_DEFAULTS_ASYNC = "FirebaseRemoteConfig.setDefaultsAsync"; + private static final String FN_SET_CONFIG_SETTINGS_ASYNC = + "FirebaseRemoteConfig.setConfigSettingsAsync"; + + FirebaseRemoteConfig() {} + + public static FirebaseRemoteConfig getInstance() { + return new FirebaseRemoteConfig(); + } + + public boolean activateFetched() { + ConfigRow row = ConfigAndroid.get(FN_ACTIVATE_FETCHED); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult(FN_ACTIVATE_FETCHED, String.valueOf(result)); + return result; + } + + public void setDefaults(int resourceId) { + FakeReporter.addReport(FN_SET_DEFAULTS, Integer.toString(resourceId)); + } + + public void setDefaults(int resourceId, String namespace) { + FakeReporter.addReport(FN_SET_DEFAULTS, Integer.toString(resourceId), namespace); + } + + public void setDefaults(Map defaults) { + Map sorted = new TreeMap<>(defaults); + FakeReporter.addReport(FN_SET_DEFAULTS, sorted.toString()); + } + + public void setDefaults(Map defaults, String namespace) { + Map sorted = new TreeMap<>(defaults); + FakeReporter.addReport(FN_SET_DEFAULTS, sorted.toString(), namespace); + } + + public void setConfigSettings(FirebaseRemoteConfigSettings settings) { + FakeReporter.addReport(FN_SET_CONFIG_SETTINGS); + } + + public long getLong(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_LONG); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult(FN_GET_LONG, Long.toString(result), key); + return result; + } + + public long getLong(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_LONG); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult(FN_GET_LONG, Long.toString(result), key, namespace); + return result; + } + + public byte[] getByteArray(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_BYTE_ARRAY); + byte[] result = row.returnvalue().tstring().getBytes(UTF_8); + FakeReporter.addReportWithResult(FN_GET_BYTE_ARRAY, new String(result), key); + return result; + } + + public byte[] getByteArray(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_BYTE_ARRAY); + byte[] result = row.returnvalue().tstring().getBytes(UTF_8); + FakeReporter.addReportWithResult(FN_GET_BYTE_ARRAY, new String(result), key, namespace); + return result; + } + + public String getString(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_STRING); + String result = row.returnvalue().tstring(); + FakeReporter.addReportWithResult(FN_GET_STRING, result, key); + return result; + } + + public String getString(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_STRING); + String result = row.returnvalue().tstring(); + FakeReporter.addReportWithResult(FN_GET_STRING, result, key, namespace); + return result; + } + + public boolean getBoolean(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_BOOLEAN); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult(FN_GET_BOOLEAN, String.valueOf(result), key); + return result; + } + + public boolean getBoolean(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_BOOLEAN); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult(FN_GET_BOOLEAN, String.valueOf(result), key, namespace); + return result; + } + + public double getDouble(String key) { + ConfigRow row = ConfigAndroid.get(FN_GET_DOUBLE); + double result = row.returnvalue().tdouble(); + FakeReporter.addReportWithResult(FN_GET_DOUBLE, String.format("%.3f", result), key); + return result; + } + + public double getDouble(String key, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_DOUBLE); + double result = row.returnvalue().tdouble(); + FakeReporter.addReportWithResult(FN_GET_DOUBLE, String.format("%.3f", result), key, namespace); + return result; + } + + public FirebaseRemoteConfigValue getValue(String key) { + FakeReporter.addReport(FN_GET_VALUE, key); + return new FirebaseRemoteConfigValue(); + } + + public FirebaseRemoteConfigValue getValue(String key, String namespace) { + FakeReporter.addReport(FN_GET_VALUE, key, namespace); + return new FirebaseRemoteConfigValue(); + } + + public FirebaseRemoteConfigInfo getInfo() { + FakeReporter.addReport(FN_GET_INFO); + return new FirebaseRemoteConfigInfo(); + } + + public Set getKeysByPrefix(String prefix) { + ConfigRow row = ConfigAndroid.get(FN_GET_KEYS_BY_PREFIX); + Set result = new TreeSet<>(stringToStringList(row.returnvalue().tstring())); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfig.getKeysByPrefix", result.toString(), prefix); + return result; + } + + public Set getKeysByPrefix(String prefix, String namespace) { + ConfigRow row = ConfigAndroid.get(FN_GET_KEYS_BY_PREFIX); + Set result = new TreeSet<>(stringToStringList(row.returnvalue().tstring())); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfig.getKeysByPrefix", result.toString(), prefix, namespace); + return result; + } + + public Map getAll() { + FakeReporter.addReport(FN_GET_ALL); + return new HashMap<>(); + } + + private static Task voidHelper(String configKey) { + Task result = Task.forResult(configKey, null); + TickerAndroid.register(result); + return result; + } + + public Task fetch() { + FakeReporter.addReport(FN_FETCH); + return voidHelper(FN_FETCH); + } + + public Task fetch(long cacheExpirationSeconds) { + FakeReporter.addReport(FN_FETCH, Long.toString(cacheExpirationSeconds)); + return voidHelper(FN_FETCH); + } + + private static Task eIHelper(String configKey) { + Task result = + Task.forResult(configKey, new FirebaseRemoteConfigInfo()); + TickerAndroid.register(result); + return result; + } + + public Task ensureInitialized() { + FakeReporter.addReport(FN_ENSURE_INITIALIZED); + return eIHelper(FN_ENSURE_INITIALIZED); + } + + private static Task booleanHelper(String configKey) { + Task result = Task.forResult(configKey, Boolean.TRUE); + TickerAndroid.register(result); + return result; + } + + public Task activate() { + FakeReporter.addReport(FN_ACTIVATE); + return booleanHelper(FN_ACTIVATE); + } + + public Task fetchAndActivate() { + FakeReporter.addReport(FN_FETCH_AND_ACTIVATE); + return booleanHelper(FN_FETCH_AND_ACTIVATE); + } + + public Task setDefaultsAsync(int resourceId) { + FakeReporter.addReport(FN_SET_DEFAULTS_ASYNC, Integer.toString(resourceId)); + return voidHelper(FN_SET_DEFAULTS_ASYNC); + } + + public Task setDefaultsAsync(Map defaults) { + Map sorted = new TreeMap<>(defaults); + FakeReporter.addReport(FN_SET_DEFAULTS_ASYNC, sorted.toString()); + return voidHelper(FN_SET_DEFAULTS_ASYNC); + } + + public Task setConfigSettingsAsync(FirebaseRemoteConfigSettings settings) { + FakeReporter.addReport(FN_SET_CONFIG_SETTINGS_ASYNC); + return voidHelper(FN_SET_CONFIG_SETTINGS_ASYNC); + } + + private static List stringToStringList(String s) { + s = s.substring(1, s.length() - 1); + if (s.length() == 0) { + return new ArrayList(); + } + String[] arr = s.split(","); + for (int i = 0; i < arr.length; i++) { + arr[i] = arr[i].trim(); + } + return Arrays.asList(arr); + } + +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java new file mode 100644 index 0000000000..8e72f08587 --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchThrottledException.java @@ -0,0 +1,24 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +/** Fake FirebaseRemoteConfigFetchThrottledException */ +public class FirebaseRemoteConfigFetchThrottledException { + + public long getThrottleEndTimeMillis() { + return 0; + } + +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java new file mode 100644 index 0000000000..5c61295168 --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigInfo.java @@ -0,0 +1,44 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; + +/** Fake FirebaseRemoteConfigInfo */ +public class FirebaseRemoteConfigInfo { + + public long getFetchTimeMillis() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigInfo.getFetchTimeMillis"); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigInfo.getFetchTimeMillis", Long.toString(result)); + return result; + } + + public int getLastFetchStatus() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigInfo.getLastFetchStatus"); + int result = row.returnvalue().tint(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigInfo.getLastFetchStatus", Integer.toString(result)); + return result; + } + + public FirebaseRemoteConfigSettings getConfigSettings() { + FakeReporter.addReport("FirebaseRemoteConfigInfo.getConfigSettings"); + return new FirebaseRemoteConfigSettings(); + } +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java new file mode 100644 index 0000000000..aa4c1d229d --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.java @@ -0,0 +1,45 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; + +/** Fake FirebaseRemoteConfigSettings */ +public class FirebaseRemoteConfigSettings { + + public boolean isDeveloperModeEnabled() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigSettings.isDeveloperModeEnabled"); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigSettings.isDeveloperModeEnabled", String.valueOf(result)); + return result; + } + + /** Fake Builder */ + public static class Builder { + public Builder setDeveloperModeEnabled(boolean enabled) { + FakeReporter.addReport( + "FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled", String.valueOf(enabled)); + return this; + } + + public FirebaseRemoteConfigSettings build() { + FakeReporter.addReport("FirebaseRemoteConfigSettings.Builder.build"); + return new FirebaseRemoteConfigSettings(); + } + } +} diff --git a/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java new file mode 100644 index 0000000000..1095739e48 --- /dev/null +++ b/remote_config/src_java/fake/com/google/firebase/remoteconfig/FirebaseRemoteConfigValue.java @@ -0,0 +1,72 @@ +// Copyright 2017 Google LLC +// +// 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. + +package com.google.firebase.remoteconfig; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.firebase.testing.cppsdk.ConfigAndroid; +import com.google.firebase.testing.cppsdk.ConfigRow; +import com.google.firebase.testing.cppsdk.FakeReporter; + +/** Fake FirebaseRemoteConfigValue */ +public class FirebaseRemoteConfigValue { + + public FirebaseRemoteConfigValue() {} + + public long asLong() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asLong"); + long result = row.returnvalue().tlong(); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asLong", Long.toString(result)); + return result; + } + + public double asDouble() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asDouble"); + double result = row.returnvalue().tdouble(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigValue.asDouble", String.format("%.3f", result)); + return result; + } + + public String asString() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asString"); + String result = row.returnvalue().tstring(); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asString", result); + return result; + } + + public byte[] asByteArray() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asByteArray"); + byte[] result = {}; + result = row.returnvalue().tstring().getBytes(UTF_8); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asByteArray", new String(result)); + return result; + } + + public boolean asBoolean() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.asBoolean"); + boolean result = row.returnvalue().tbool(); + FakeReporter.addReportWithResult("FirebaseRemoteConfigValue.asBoolean", String.valueOf(result)); + return result; + }; + + public int getSource() { + ConfigRow row = ConfigAndroid.get("FirebaseRemoteConfigValue.getSource"); + int result = row.returnvalue().tint(); + FakeReporter.addReportWithResult( + "FirebaseRemoteConfigValue.getSource", Integer.toString(result)); + return result; + } +} diff --git a/remote_config/tests/CMakeLists.txt b/remote_config/tests/CMakeLists.txt new file mode 100644 index 0000000000..6f5b7433db --- /dev/null +++ b/remote_config/tests/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright 2019 Google LLC +# +# 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. + +# TODO: This test is currently Android-only and needs extra java deps. +# TODO: Work out how to make it work for desktop +#[[ +firebase_cpp_cc_test( + firebase_remote_config_test + SOURCES + remote_config_test.cc + DEPENDS + firebase_app_for_testing + firebase_remote_config + firebase_testing +) +]] + +firebase_cpp_cc_test( + firebase_remote_config_desktop_config_data_test + SOURCES + desktop/config_data_test.cc + DEPENDS + firebase_app_for_testing + firebase_remote_config + firebase_testing +) diff --git a/remote_config/tests/desktop/config_data_test.cc b/remote_config/tests/desktop/config_data_test.cc new file mode 100644 index 0000000000..b8792c6b5c --- /dev/null +++ b/remote_config/tests/desktop/config_data_test.cc @@ -0,0 +1,156 @@ +// Copyright 2018 Google LLC +// +// 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 "remote_config/src/desktop/config_data.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +TEST(LayeredConfigsTest, Convertation) { + NamespacedConfigData fetched( + NamespaceKeyValueMap( + {{"namespace1", {{"key1", "value1"}, {"key2", "value2"}}}}), + 1234567); + NamespacedConfigData active( + NamespaceKeyValueMap( + {{"namespace2", {{"key1", "value1"}, {"key2", "value2"}}}}), + 5555555); + NamespacedConfigData defaults( + NamespaceKeyValueMap( + {{"namespace3", {{"key1", "value1"}, {"key2", "value2"}}}}), + 9999999); + + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "0"); + + LayeredConfigs configs(fetched, active, defaults, metadata); + std::string buffer = configs.Serialize(); + LayeredConfigs new_configs; + new_configs.Deserialize(buffer); + + EXPECT_EQ(configs, new_configs); +} + +TEST(NamespacedConfigDataTest, ConversionToFlexbuffer) { + NamespacedConfigData config_data( + NamespaceKeyValueMap( + {{"namespace1", {{"key1", "value1"}, {"key2", "value2"}}}}), + 1234567); + + // Serialize the data to a string + std::string buffer = config_data.Serialize(); + + // Make a new config and deserialize it with the string. + NamespacedConfigData new_config_data; + new_config_data.Deserialize(buffer); + + EXPECT_EQ(config_data, new_config_data); +} + +TEST(NamespacedConfigDataTest, DefaultConstructor) { + NamespacedConfigData holder1; + NamespacedConfigData holder2(NamespaceKeyValueMap(), 0); + EXPECT_EQ(holder1, holder2); +} + +TEST(NamespacedConfigDataTest, SetNamespace) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 0); + EXPECT_EQ(holder.GetValue("key1", "namespace1"), "value1"); + + holder.SetNamespace(std::map({{"key2", "value2"}}), + "namespace1"); + + EXPECT_FALSE(holder.HasValue("key1", "namespace1")); + EXPECT_EQ(holder.GetValue("key2", "namespace1"), "value2"); +} + +TEST(NamespacedConfigDataTest, HasValue) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 0); + EXPECT_TRUE(holder.HasValue("key1", "namespace1")); + EXPECT_FALSE(holder.HasValue("key2", "namespace1")); + EXPECT_FALSE(holder.HasValue("key3", "namespace2")); +} + +TEST(NamespacedConfigDataTest, HasValueEmpty) { + NamespacedConfigData holder(NamespaceKeyValueMap(), 0); + EXPECT_FALSE(holder.HasValue("key1", "namespace1")); + EXPECT_FALSE(holder.HasValue("key2", "namespace1")); + EXPECT_FALSE(holder.HasValue("key1", "namespace2")); + EXPECT_FALSE(holder.HasValue("key3", "namespace3")); +} + +TEST(NamespacedConfigDataTest, GetValue) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 0); + EXPECT_EQ(holder.GetValue("key1", "namespace1"), "value1"); + EXPECT_EQ(holder.GetValue("key2", "namespace1"), ""); + EXPECT_EQ(holder.GetValue("key3", "namespace2"), ""); + EXPECT_EQ(holder.GetValue("key4", "namespace2"), ""); +} + +TEST(NamespacedConfigDataTest, GetValueEmpty) { + NamespacedConfigData holder(NamespaceKeyValueMap(), 0); + EXPECT_EQ(holder.GetValue("key1", "namespace1"), ""); + EXPECT_EQ(holder.GetValue("key2", "namespace2"), ""); +} + +TEST(NamespacedConfigDataTest, GetKeysByPrefix) { + NamespaceKeyValueMap m( + {{"namespace1", + {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}}}); + NamespacedConfigData holder(m, 0); + std::set keys; + holder.GetKeysByPrefix("key", "namespace1", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre("key1", "key2", "key3")); + keys.clear(); + + holder.GetKeysByPrefix("", "namespace1", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre("key1", "key2", "key3")); + keys.clear(); + + holder.GetKeysByPrefix("some_other_key", "namespace1", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre()); + keys.clear(); + + holder.GetKeysByPrefix("some_prefix", "namespace2", &keys); + EXPECT_THAT(keys, ::testing::UnorderedElementsAre()); + keys.clear(); +} + +TEST(NamespacedConfigDataTest, GetConfig) { + NamespaceKeyValueMap m( + {{"namespace1", + {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}}}); + NamespacedConfigData holder(m, 1498757224); + EXPECT_EQ(holder.config(), m); +} + +TEST(NamespacedConfigDataTest, GetTimestamp) { + NamespaceKeyValueMap m({{"namespace1", {{"key1", "value1"}}}}); + NamespacedConfigData holder(m, 1498757224); + EXPECT_EQ(holder.timestamp(), 1498757224); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/file_manager_test.cc b/remote_config/tests/desktop/file_manager_test.cc new file mode 100644 index 0000000000..591d016c8c --- /dev/null +++ b/remote_config/tests/desktop/file_manager_test.cc @@ -0,0 +1,66 @@ +// Copyright 2017 Google LLC +// +// 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 + +#include "testing/base/public/googletest.h" +#include "gtest/gtest.h" + +#include "file/base/path.h" +#include "remote_config/src/desktop/config_data.h" +#include "remote_config/src/desktop/file_manager.h" +#include "remote_config/src/desktop/metadata.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +TEST(RemoteConfigFileManagerTest, SaveAndLoadSuccess) { + std::string file_path = + file::JoinPath(FLAGS_test_tmpdir, "remote_config_data"); + + RemoteConfigFileManager file_manager(file_path); + NamespacedConfigData fetched( + NamespaceKeyValueMap( + {{"namespace1", {{"key1", "value1"}, {"key2", "value2"}}}}), + 1234567); + NamespacedConfigData active( + NamespaceKeyValueMap( + {{"namespace2", {{"key1", "value1"}, {"key2", "value2"}}}}), + 5555555); + NamespacedConfigData defaults( + NamespaceKeyValueMap( + {{"namespace3", {{"key1", "value1"}, {"key2", "value2"}}}}), + 9999999); + + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "0"); + + LayeredConfigs configs(fetched, active, defaults, metadata); + + EXPECT_TRUE(file_manager.Save(configs)); + + LayeredConfigs new_configs; + EXPECT_TRUE(file_manager.Load(&new_configs)); + EXPECT_EQ(configs, new_configs); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/metadata_test.cc b/remote_config/tests/desktop/metadata_test.cc new file mode 100644 index 0000000000..4de1570a3b --- /dev/null +++ b/remote_config/tests/desktop/metadata_test.cc @@ -0,0 +1,101 @@ +// Copyright 2017 Google LLC +// +// 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 + +#include "gtest/gtest.h" + +#include "remote_config/src/desktop/metadata.h" +#include "remote_config/src/include/firebase/remote_config.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +void ExpectEqualConfigInfo(const ConfigInfo& l, const ConfigInfo& r) { + EXPECT_EQ(l.fetch_time, r.fetch_time); + EXPECT_EQ(l.last_fetch_status, r.last_fetch_status); + EXPECT_EQ(l.last_fetch_failure_reason, r.last_fetch_failure_reason); + EXPECT_EQ(l.throttled_end_time, r.throttled_end_time); +} + +TEST(RemoteConfigMetadataTest, Serialization) { + RemoteConfigMetadata remote_config_metadata; + remote_config_metadata.set_info( + ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + remote_config_metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + remote_config_metadata.AddSetting(kConfigSettingDeveloperMode, "0"); + + std::string buffer = remote_config_metadata.Serialize(); + RemoteConfigMetadata new_remote_config_metadata; + new_remote_config_metadata.Deserialize(buffer); + + EXPECT_EQ(remote_config_metadata, new_remote_config_metadata); +} + +TEST(RemoteConfigMetadataTest, GetInfoDefaultValues) { + RemoteConfigMetadata m; + ExpectEqualConfigInfo(m.info(), ConfigInfo({0, kLastFetchStatusSuccess, + kFetchFailureReasonInvalid, 0})); +} + +TEST(RemoteConfigMetadataTest, SetAndGetInfo) { + ConfigInfo info = {1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888}; + RemoteConfigMetadata m; + m.set_info(info); + ExpectEqualConfigInfo(m.info(), info); +} + +TEST(RemoteConfigMetadataTest, SetAndGetDigest) { + MetaDigestMap digest({{"namespace1", "digest1"}, {"namespace2", "digest2"}}); + + RemoteConfigMetadata m; + m.set_digest_by_namespace(digest); + + EXPECT_EQ(m.digest_by_namespace(), digest); +} + +TEST(RemoteConfigMetadataTest, SetAndGetSetting) { + RemoteConfigMetadata m; + EXPECT_EQ(m.GetSetting(kConfigSettingDeveloperMode), "0"); + + m.AddSetting(kConfigSettingDeveloperMode, "0"); + EXPECT_EQ(m.GetSetting(kConfigSettingDeveloperMode), "0"); + + m.AddSetting(kConfigSettingDeveloperMode, "1"); + EXPECT_EQ(m.GetSetting(kConfigSettingDeveloperMode), "1"); +} + +TEST(RemoteConfigMetadataTest, SetAndsettings) { + RemoteConfigMetadata m; + + std::map map; + EXPECT_EQ(m.settings(), map); + + m.AddSetting(kConfigSettingDeveloperMode, "0"); + map[kConfigSettingDeveloperMode] = "0"; + EXPECT_EQ(m.settings(), map); + + m.AddSetting(kConfigSettingDeveloperMode, "1"); + map[kConfigSettingDeveloperMode] = "1"; + EXPECT_EQ(m.settings(), map); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/notification_channel_test.cc b/remote_config/tests/desktop/notification_channel_test.cc new file mode 100644 index 0000000000..724215138d --- /dev/null +++ b/remote_config/tests/desktop/notification_channel_test.cc @@ -0,0 +1,79 @@ +// Copyright 2017 Google LLC +// +// 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 // NOLINT +#include // NOLINT +#include // NOLINT +#include "gtest/gtest.h" + +#include "remote_config/src/desktop/notification_channel.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +class NotificationChannelTest : public ::testing::Test { + protected: + int times_ = 0; + NotificationChannel channel_; +}; + +TEST_F(NotificationChannelTest, All) { + std::thread thread([this]() { + while (channel_.Get()) { + times_++; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } + }); + + EXPECT_EQ(times_, 0); + + // Thread will get `notification`. + channel_.Put(); + // Thread will get `notification` in short period of time + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // Expect thread got one notification. Thread is processing something now. + EXPECT_EQ(times_, 1); + + // Thread will get `notification` afrer current loop iteration. + channel_.Put(); + // Thread will get notification in short period of time + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // Expect thread got one `notification` total. It is processing something. + EXPECT_EQ(times_, 1); + + // Thread is doing something. It will get notification after finish first loop + // iteration. So channel will ignore this Put() call. + channel_.Put(); + // Thread will finish second loop iteration after sleep. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + // Expect thread got two `notification`s total. + EXPECT_EQ(times_, 2); + + // Thread will get notification that channel is closed. Thread will be closed. + channel_.Close(); + // Wait until thread will get `close notification`. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + // Thread should be closed, because channel is closed. + channel_.Put(); + // Still expect that thread got two `notification`s total. + EXPECT_EQ(times_, 2); + + thread.join(); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/remote_config_desktop_test.cc b/remote_config/tests/desktop/remote_config_desktop_test.cc new file mode 100644 index 0000000000..4902a663a6 --- /dev/null +++ b/remote_config/tests/desktop/remote_config_desktop_test.cc @@ -0,0 +1,528 @@ +// Copyright 2017 Google LLC +// +// 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 "remote_config/src/desktop/remote_config_desktop.h" + +#include // NOLINT + +#include "file/base/path.h" +#include "firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "firebase/future.h" +#include "remote_config/src/common.h" +#include "remote_config/src/desktop/config_data.h" +#include "remote_config/src/desktop/file_manager.h" +#include "remote_config/src/desktop/metadata.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +class RemoteConfigDesktopTest : public ::testing::Test { + protected: + void SetUp() override { + app_ = testing::CreateApp(); + + FutureData::Create(); + file_manager_ = new RemoteConfigFileManager( + file::JoinPath(FLAGS_test_tmpdir, "remote_config_data")); + SetUpInstance(); + } + + void TearDown() override { + delete instance_; + delete configs_; + delete file_manager_; + FutureData::Destroy(); + delete app_; + } + + // Remove previous instance and create the new one. New instance will load + // data from file, so we need to create file with data. + // + // After calling this function the `instance->configs_` must to be equal to + // the `configs_`. + void SetUpInstance() { + // !!! Remove previous instance at first, because Client can save data in + // background when you will rewriting the same file. + delete instance_; + SetupContent(); + EXPECT_TRUE(file_manager_->Save(*configs_)); + instance_ = new RemoteConfigInternal(*app_, *file_manager_); + } + + // Remove previous content and create the new one. + void SetupContent() { + uint64_t milliseconds_since_epoch = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + // Set this timestamp to guarantee passing fetching conditions. + NamespacedConfigData fetched( + NamespaceKeyValueMap( + {{"namespace2", {{"key1", "value1"}, {"key2", "value2"}}}}), + milliseconds_since_epoch - 2 * 1000 * kDefaultCacheExpiration); + NamespacedConfigData active( + NamespaceKeyValueMap({{RemoteConfigInternal::kDefaultNamespace, + {{"key_bool", "f"}, + {"key_long", "55555"}, + {"key_double", "100.5"}, + {"key_string", "aaa"}, + {"key_data", "zzz"}}}}), + 1234567); + NamespacedConfigData defaults( + NamespaceKeyValueMap({}), + 9999999); + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo({1498757224, kLastFetchStatusPending, + kFetchFailureReasonThrottled, 1498758888})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"namespace1", "digest1"}, {"namespace2", "digest2"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "1"); + + delete configs_; + configs_ = new LayeredConfigs(fetched, active, defaults, metadata); + } + + firebase::App* app_ = nullptr; + + RemoteConfigInternal* instance_ = nullptr; + LayeredConfigs* configs_ = nullptr; + RemoteConfigFileManager* file_manager_ = nullptr; +}; + +// Can't load `configs_` from file without permissions. +TEST_F(RemoteConfigDesktopTest, FailedLoadFromFile) { + RemoteConfigInternal instance( + *app_, RemoteConfigFileManager( + file::JoinPath(FLAGS_test_tmpdir, "not_found_file"))); + EXPECT_EQ(LayeredConfigs(), instance.configs_); +} + +TEST_F(RemoteConfigDesktopTest, SuccessLoadFromFile) { + EXPECT_EQ(*configs_, instance_->configs_); +} + +// Check async saving working well. +TEST_F(RemoteConfigDesktopTest, SuccessAsyncSaveToFile) { + // Let change the `configs_` variable. + instance_->configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap( + {{"new_namespace1", + {{"new_key1", "new_value1"}, {"new_key2", "new_value2"}}}}), + 999999); + + instance_->save_channel_.Put(); + + // Need to wait until background thread will save `configs_` to the file. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + LayeredConfigs new_content; + EXPECT_TRUE(file_manager_->Load(&new_content)); + EXPECT_EQ(new_content, instance_->configs_); +} + +TEST_F(RemoteConfigDesktopTest, SetDefaultsKeyValueVariant) { + { + SetUpInstance(); + + Variant vector_variant; + std::vector* std_vector_variant = + new std::vector(1, Variant::FromMutableBlob("123", 4)); + vector_variant.AssignVector(&std_vector_variant); + + ConfigKeyValueVariant defaults[] = { + ConfigKeyValueVariant{"key_bool", Variant(true)}, + ConfigKeyValueVariant{"key_blob", + Variant::FromMutableBlob("123456789", 9)}, + ConfigKeyValueVariant{"key_string", Variant("black")}, + ConfigKeyValueVariant{"key_long", Variant(120)}, + ConfigKeyValueVariant{"key_double", Variant(600.5)}, + // Will be ignored, this type is not supported. + ConfigKeyValueVariant{"key_vector_variant", vector_variant}}; + + instance_->SetDefaults(defaults, 6); + configs_->defaults.SetNamespace( + { + {"key_bool", "true"}, + {"key_blob", "123456789"}, + {"key_string", "black"}, + {"key_long", "120"}, + {"key_double", "600.5000000000000000"}, + }, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } + { + SetUpInstance(); + // `defaults` contains two keys `height`. The last one must to be applied. + ConfigKeyValueVariant defaults[] = { + ConfigKeyValueVariant{"height", Variant(100)}, + ConfigKeyValueVariant{"height", Variant(500)}, + ConfigKeyValueVariant{"width", Variant("120cm")}}; + instance_->SetDefaults(defaults, 3); + configs_->defaults.SetNamespace({{"height", "500"}, {"width", "120cm"}}, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } +} + +TEST_F(RemoteConfigDesktopTest, SetDefaultsKeyValue) { + { + SetUpInstance(); + ConfigKeyValue defaults[] = {ConfigKeyValue{"height", "100"}, + ConfigKeyValue{"height", "500"}, + ConfigKeyValue{"width", "120cm"}}; + instance_->SetDefaults(defaults, 3); + configs_->defaults.SetNamespace({{"height", "500"}, {"width", "120cm"}}, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } + { + SetUpInstance(); + ConfigKeyValue defaults[] = {ConfigKeyValue{"height", "100"}, + ConfigKeyValue{"height", "500"}, + ConfigKeyValue{"width", "120cm"}}; + instance_->SetDefaults(defaults, 3); + configs_->defaults.SetNamespace({{"height", "500"}, {"width", "120cm"}}, + RemoteConfigInternal::kDefaultNamespace); + EXPECT_EQ(*configs_, instance_->configs_); + } +} + +TEST_F(RemoteConfigDesktopTest, GetAndSetConfigSetting) { + EXPECT_EQ(instance_->GetConfigSetting(kConfigSettingDeveloperMode), "1"); + instance_->SetConfigSetting(kConfigSettingDeveloperMode, "0"); + EXPECT_EQ(instance_->GetConfigSetting(kConfigSettingDeveloperMode), "0"); +} + +TEST_F(RemoteConfigDesktopTest, GetBoolean) { + { EXPECT_FALSE(instance_->GetBoolean("key_bool", nullptr)); } + { + ValueInfo info; + EXPECT_FALSE(instance_->GetBoolean("key_bool", &info)); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetLong) { + { EXPECT_EQ(instance_->GetLong("key_long", nullptr), 55555); } + { + ValueInfo info; + EXPECT_EQ(instance_->GetLong("key_long", &info), 55555); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetDouble) { + { EXPECT_EQ(instance_->GetDouble("key_double", nullptr), 100.5); } + { + ValueInfo info; + EXPECT_EQ(instance_->GetDouble("key_double", &info), 100.5); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetString) { + { EXPECT_EQ(instance_->GetString("key_string", nullptr), "aaa"); } + { + ValueInfo info; + EXPECT_EQ(instance_->GetString("key_string", &info), "aaa"); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetData) { + { + EXPECT_THAT(instance_->GetData("key_data", nullptr), + ::testing::Eq(std::vector{'z', 'z', 'z'})); + } + { + ValueInfo info; + EXPECT_THAT(instance_->GetData("key_data", &info), + ::testing::Eq(std::vector{'z', 'z', 'z'})); + EXPECT_TRUE(info.conversion_successful); + EXPECT_EQ(info.source, kValueSourceRemoteValue); + } +} + +TEST_F(RemoteConfigDesktopTest, GetKeys) { + { + EXPECT_THAT( + instance_->GetKeys(), + ::testing::Eq(std::vector{ + "key_bool", "key_data", "key_double", "key_long", "key_string"})); + } +} + +TEST_F(RemoteConfigDesktopTest, GetKeysByPrefix) { + { + EXPECT_THAT( + instance_->GetKeysByPrefix("key"), + ::testing::Eq(std::vector{ + "key_bool", "key_data", "key_double", "key_long", "key_string"})); + } + { + EXPECT_THAT( + instance_->GetKeysByPrefix("key_d"), + ::testing::Eq(std::vector{"key_data", "key_double"})); + } +} + +TEST_F(RemoteConfigDesktopTest, GetInfo) { + ConfigInfo info = instance_->GetInfo(); + EXPECT_EQ(info.fetch_time, 1498757224); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusPending); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonThrottled); + EXPECT_EQ(info.throttled_end_time, 1498758888); +} + +TEST_F(RemoteConfigDesktopTest, ActivateFetched) { + { + SetUpInstance(); + + instance_->configs_.fetched = NamespacedConfigData(); + instance_->configs_.active = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace:active", {{"key", "aaa"}}}}), 999999); + + // Will not activate, because the `fetched` configs is empty. + EXPECT_FALSE(instance_->ActivateFetched()); + } + { + SetUpInstance(); + + instance_->configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "aaa"}}}}), 999999); + instance_->configs_.active = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "aaa"}}}}), 999999); + + // Will not activate, because the `fetched` configs equal to the `active` + // configs, they have the same timestamp. + EXPECT_FALSE(instance_->ActivateFetched()); + } + { + SetUpInstance(); + instance_->configs_.fetched = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace:fetched", {{"key1", "aaa"}}}}), + 9999999999); + instance_->configs_.active = NamespacedConfigData( + NamespaceKeyValueMap({{"namespace:active", {{"key2", "zzz"}}}}), + 999999); + + // Will activate, because the `fetched` configs timestamp more than the + // `active` configs timestamp. + EXPECT_TRUE(instance_->ActivateFetched()); + EXPECT_EQ(instance_->configs_.fetched, instance_->configs_.active); + } +} + +TEST_F(RemoteConfigDesktopTest, Fetch) { + // Use fake rest implementation. In fake we just return some other metadata + // and fetched config and don't make HTTP requests. In this test case want + // make sure that all updated values apply correctly. + // + // See rest_fake.cc for more details. + { + SetUpInstance(); + instance_->Fetch(0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ(instance_->configs_.fetched, + NamespacedConfigData( + NamespaceKeyValueMap({{"namespace", {{"key", "value"}}}}), + 1000000)); + + EXPECT_EQ(instance_->configs_.metadata.digest_by_namespace(), + MetaDigestMap({{"namespace", "digest"}})); + + ConfigInfo info = instance_->configs_.metadata.info(); + EXPECT_EQ(info.fetch_time, 0); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusSuccess); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonError); + EXPECT_EQ(info.throttled_end_time, 0); + + EXPECT_EQ( + instance_->configs_.metadata.GetSetting(kConfigSettingDeveloperMode), + "1"); + } + { + // Will fetch, because cache_expiration_in_seconds == 0. + SetUpInstance(); + Future future = instance_->Fetch(0); + EXPECT_EQ(future.status(), firebase::kFutureStatusPending); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(future.status(), firebase::kFutureStatusComplete); + } + { + // Will fetch, because cache is older than cache_expiration_in_seconds. + // We setup fetch.timestamp as + // milliseconds_since_epoch - 2*1000*cache_expiration_in_seconds; + SetUpInstance(); + Future future = instance_->Fetch(kDefaultCacheExpiration); + EXPECT_EQ(future.status(), firebase::kFutureStatusPending); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(future.status(), firebase::kFutureStatusComplete); + } + { + // Will NOT fetch, because cache is newer than kDefaultCacheExpiration + SetUpInstance(); + Future future = instance_->Fetch(10 * kDefaultCacheExpiration); + EXPECT_EQ(future.status(), firebase::kFutureStatusComplete); + } +} + +TEST_F(RemoteConfigDesktopTest, TestIsBoolTrue) { + // Confirm all the values that ARE BoolTrue. + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("1")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("true")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("t")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("on")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("yes")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolTrue("y")); + + // Ensure all the BoolFalse values are not BoolTrue. + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("0")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("false")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("f")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("no")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("n")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("off")); + + // Confirm a few random values. + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("apple")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("Yes")); // lower case only + EXPECT_FALSE( + RemoteConfigInternal::IsBoolTrue("100")); // only the number 1 exactly + EXPECT_FALSE( + RemoteConfigInternal::IsBoolTrue("-1")); // only the number 1 exactly + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("1.0")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("True")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("False")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolTrue("N")); // lower-case only +} + +TEST_F(RemoteConfigDesktopTest, TestIsBoolFalse) { + // Ensure all the BoolFalse values are not BoolTrue. + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("0")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("false")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("f")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("no")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("n")); + EXPECT_TRUE(RemoteConfigInternal::IsBoolFalse("off")); + + // Confirm that the BoolTrue values are not BoolFalse. + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("1")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("true")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("t")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("on")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("yes")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("y")); + + // Confirm a few random values. + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("apple")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("Yes")); // lower case only + EXPECT_FALSE( + RemoteConfigInternal::IsBoolFalse("100")); // only the number 1 exactly + EXPECT_FALSE( + RemoteConfigInternal::IsBoolFalse("-1")); // only the number 1 exactly + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("1.0")); + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("True")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("False")); // lower-case only + EXPECT_FALSE(RemoteConfigInternal::IsBoolFalse("N")); // lower-case only +} + +TEST_F(RemoteConfigDesktopTest, TestIsLong) { + EXPECT_TRUE(RemoteConfigInternal::IsLong("0")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("1")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("2")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("+0")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("+3")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("-5")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("8249")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("-718129")); + EXPECT_TRUE(RemoteConfigInternal::IsLong("+9173923192819")); + + EXPECT_FALSE(RemoteConfigInternal::IsLong("0.0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong(" 5")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("9 ")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("- 8")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("-0-")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("-+0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("0-0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("1-1")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("12345+")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("12345-")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("12345abc")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("++81020")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("--32391")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("2+2=4")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("234,456")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("234.1")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("829.0")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("1e100")); + EXPECT_FALSE(RemoteConfigInternal::IsLong("")); + EXPECT_FALSE(RemoteConfigInternal::IsLong(" ")); +} + +TEST_F(RemoteConfigDesktopTest, TestIsDouble) { + EXPECT_TRUE(RemoteConfigInternal::IsDouble("0")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("2")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+0")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+3")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-5")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1.")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("8249")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-718129")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+9173923192819")); + + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1e10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("1.2e9729")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("48.3e-39")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble(".4e+9")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-.289e11")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-7293e+72")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+489e322")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("10E10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("10E-10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("-10E+10")); + EXPECT_TRUE(RemoteConfigInternal::IsDouble("+10E-10")); + + EXPECT_FALSE(RemoteConfigInternal::IsDouble("1.2e")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("1.9.2")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("1.3e8e2")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("-13-e8")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("98e4.3")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble(" 1")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("8 ")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("56.8f-29")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("-793e+89apple")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("489EEE")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("489EEE123")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble(" ")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble("e")); + EXPECT_FALSE(RemoteConfigInternal::IsDouble(".")); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/desktop/rest_test.cc b/remote_config/tests/desktop/rest_test.cc new file mode 100644 index 0000000000..4ec0776bdd --- /dev/null +++ b/remote_config/tests/desktop/rest_test.cc @@ -0,0 +1,502 @@ +// Copyright 2017 Google LLC +// +// 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 "remote_config/src/desktop/rest.h" + +#include +#include + +#include "app/rest/transport_builder.h" +#include "app/rest/transport_interface.h" +#include "app/rest/transport_mock.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "remote_config/src/desktop/rest_nanopb_encode.h" +#include "testing/config.h" +#include "net/proto2/public/text_format.h" +#include "zlib/zlibwrapper.h" +#include "wireless/android/config/proto/config.proto.h" + +namespace firebase { +namespace remote_config { +namespace internal { + +class RemoteConfigRESTTest : public ::testing::Test { + protected: + void SetUp() override { + // Use TransportMock for testing instead of TransportCurl + rest::SetTransportBuilder([]() -> std::unique_ptr { + return std::unique_ptr(new rest::TransportMock); + }); + + firebase::AppOptions options = testing::MockAppOptions(); + options.set_package_name("com.google.samples.quickstart.config"); + options.set_app_id("1:290292664153:android:eddef00f8bd18e11"); + + app_ = testing::CreateApp(options); + + SetupContent(); + SetupProtoResponse(); + } + + void TearDown() override { delete app_; } + + void SetupContent() { + std::map empty_map; + NamespacedConfigData fetched( + NamespaceKeyValueMap({ + {"star_wars:droid", + {{"name", "BB-8"}, + {"height", "0.67 meters"}, + {"mass", "18 kilograms"}}}, + {"star_wars:starship", + {{"name", "Millennium Falcon"}, + {"length", "34.52–34.75 meters"}, + {"maximum_atmosphere_speed", "1,050 km/h"}}}, + {"star_wars:films", empty_map}, + {"star_wars:creatures", + {{"name", "Wampa"}, + {"height", "3 meters"}, + {"mass", "150 kilograms"}}}, + {"star_wars:locations", + {{"name", "Coruscant"}, + {"rotation_period", "24 standard hours"}, + {"orbital_period", "365 standard days"}}}, + }), + MillisecondsSinceEpoch() - 7 * 3600 * 1000); // 7 hours ago. + NamespacedConfigData active( + NamespaceKeyValueMap({{"star_wars:droid", + {{"name", "R2-D2"}, + {"height", "1.09 meters"}, + {"mass", "32 kilograms"}}}, + {"star_wars:starship", + {{"name", "Imperial I-class Star Destroyer"}, + {"length", "1,600 meters"}, + {"maximum_atmosphere_speed", "975 km/h"}}}}), + MillisecondsSinceEpoch() - 10 * 3600 * 1000); // 10 hours ago. + // Can be empty for testing. + NamespacedConfigData defaults(NamespaceKeyValueMap(), 0); + + RemoteConfigMetadata metadata; + metadata.set_info(ConfigInfo( + {MillisecondsSinceEpoch() - 7 * 3600 * 1000 /* 7 hours ago */, + kLastFetchStatusSuccess, kFetchFailureReasonInvalid, 0})); + metadata.set_digest_by_namespace( + MetaDigestMap({{"star_wars:droid", "DROID_DIGEST"}, + {"star_wars:starship", "STARSHIP_DIGEST"}, + {"star_wars:films", "FILMS_DIGEST"}, + {"star_wars:creatures", "CREATURES_DIGEST"}, + {"star_wars:locations", "LOCATIONS_DIGEST"}})); + metadata.AddSetting(kConfigSettingDeveloperMode, "1"); + + configs_ = LayeredConfigs(fetched, active, defaults, metadata); + } + + void SetupProtoResponse() { + std::string text = + "app_config {" + " app_name: \"com.google.samples.quickstart.config\"" + + // UPDATE, add new namespace. + " namespace_config {" + " namespace: \"star_wars:vehicle\"" + " digest: \"VEHICLE_NEW_DIGEST\"" + " status: UPDATE" + " entry {key: \"name\" value: \"All Terrain Armored Transport\"}" + " entry {key: \"passengers\" value: \"40 troops\"}" + " entry {key: \"cargo_capacity\" value: \"3,500 metric tons\"}" + " }" + + // UPDATE, update existed namespace. + " namespace_config {" + " namespace: \"star_wars:starship\"" + " digest: \"STARSHIP_NEW_DIGEST\"" + " status: UPDATE" + " entry {key: \"name\" value: \"Imperial I-class Star Destroyer\"}" + " entry {key: \"length\" value: \"1,600 meters\"}" + " entry {key: \"maximum_atmosphere_speed\" value: \"975 km/h\"}" + " }" + + // NO_TEMPLATE for existed namespace. Remove digest and namespace. + " namespace_config {" + " namespace: \"star_wars:films\" status: NO_TEMPLATE" + " }" + + // NO_TEMPLATE for NOT existed namespace. Will be ignored. + " namespace_config {" + " namespace: \"star_wars:spinoff_films\" status: NO_TEMPLATE" + " }" + + // NO_CHANGE for existed namespace. Only digest will be updated. + " namespace_config {" + " namespace: \"star_wars:droid\"" + " digest: \"DROID_NEW_DIGEST\"" + " status: NO_CHANGE" + " }" + + // EMPTY_CONFIG for existed namespace. Clear namespace and update + // digest. + " namespace_config {" + " namespace: \"star_wars:creatures\"" + " digest: \"CREATURES_NEW_DIGEST\"" + " status: EMPTY_CONFIG" + " }" + + // EMPTY_CONFIG for NOT existed namespace. Create empty namespace and + // add new digest to map. + " namespace_config {" + " namespace: \"star_wars:duels\"" + " digest: \"DUELS_NEW_DIGEST\"" + " status: EMPTY_CONFIG" + " }" + + // NOT_AUTHORIZED for existed namespace. Remove namespace and digest. + " namespace_config {" + " namespace: \"star_wars:locations\"" + " status: NOT_AUTHORIZED" + " }" + + // NOT_AUTHORIZED for NOT existed namespace. Will be ignored. + " namespace_config {" + " namespace: \"star_wars:video_games\"" + " status: NOT_AUTHORIZED" + " }" + + "}"; + + EXPECT_TRUE(proto2::TextFormat::ParseFromString(text, &proto_response_)); + } + + // This was moved from the code that used to build proto requests when + // protosbufs were used directly. It can live here because the tests can + // still depend on protobufs and gives us a way to validate nanopbs are + // encoded the same way as the original protos. + android::config::ConfigFetchRequest GetProtoFetchRequestData( + const RemoteConfigREST& rest) { + android::config::ConfigFetchRequest proto_request; + proto_request.set_client_version(2); + proto_request.set_device_type(5); + proto_request.set_device_subtype(10); + + android::config::PackageData* package_data = + proto_request.add_package_data(); + package_data->set_package_name(rest.app_package_name_); + package_data->set_gmp_project_id(rest.app_gmp_project_id_); + + for (const auto& keyvalue : rest.configs_.metadata.digest_by_namespace()) { + android::config::NamedValue* named_value = + package_data->add_namespace_digest(); + named_value->set_name(keyvalue.first); + named_value->set_value(keyvalue.second); + } + + // Check if developer mode enable + if (rest.configs_.metadata.GetSetting(kConfigSettingDeveloperMode) == "1") { + android::config::NamedValue* named_value = + package_data->add_custom_variable(); + named_value->set_name(kDeveloperModeKey); + named_value->set_value("1"); + } + + // Need iid for next two fields + // package_data->set_app_instance_id("fake instance id"); + // package_data->set_app_instance_id_token("fake instance id token"); + + package_data->set_requested_cache_expiration_seconds( + static_cast(rest.cache_expiration_in_seconds_)); + + if (rest.configs_.fetched.timestamp() == 0) { + package_data->set_fetched_config_age_seconds(-1); + } else { + package_data->set_fetched_config_age_seconds(static_cast( + (MillisecondsSinceEpoch() - rest.configs_.fetched.timestamp()) / + 1000)); + } + + package_data->set_sdk_version(SDK_MAJOR_VERSION * 10000 + + SDK_MINOR_VERSION * 100 + SDK_PATCH_VERSION); + + if (rest.configs_.active.timestamp() == 0) { + package_data->set_active_config_age_seconds(-1); + } else { + package_data->set_active_config_age_seconds(static_cast( + (MillisecondsSinceEpoch() - rest.configs_.active.timestamp()) / + 1000)); + } + return proto_request; + } + + // Check all values in case when fetch failed. + void ExpectFetchFailure(const RemoteConfigREST& rest, int code) { + EXPECT_EQ(rest.rest_response_.status(), code); + EXPECT_TRUE(rest.rest_response_.header_completed()); + EXPECT_TRUE(rest.rest_response_.body_completed()); + + EXPECT_EQ(rest.fetched().config(), configs_.fetched.config()); + EXPECT_EQ(rest.metadata().digest_by_namespace(), + configs_.metadata.digest_by_namespace()); + + ConfigInfo info = rest.metadata().info(); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusFailure); + EXPECT_LE(info.fetch_time, MillisecondsSinceEpoch()); + EXPECT_GE(info.fetch_time, MillisecondsSinceEpoch() - 10000); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonError); + EXPECT_LE(info.throttled_end_time, MillisecondsSinceEpoch()); + EXPECT_GE(info.throttled_end_time, MillisecondsSinceEpoch() - 10000); + } + + uint64_t MillisecondsSinceEpoch() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } + + std::string GzipCompress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_size = ZLib::MinCompressbufSize(input.length()); + std::unique_ptr result(new char[result_size]); + int err = zlib.Compress( + reinterpret_cast(result.get()), &result_size, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_size); + } + + std::string GzipDecompress(const std::string& input) { + ZLib zlib; + zlib.SetGzipHeaderMode(); + uLongf result_length = zlib.GzipUncompressedLength( + reinterpret_cast(input.data()), input.length()); + std::unique_ptr result(new char[result_length]); + int err = zlib.Uncompress( + reinterpret_cast(result.get()), &result_length, + reinterpret_cast(input.data()), input.length()); + EXPECT_EQ(err, Z_OK); + return std::string(result.get(), result_length); + } + + firebase::App* app_ = nullptr; + + LayeredConfigs configs_; + + rest::Response rest_response_; + android::config::ConfigFetchResponse proto_response_; +}; + +// Check correctness protobuf object setup for REST request. +TEST_F(RemoteConfigRESTTest, SetupProto) { + RemoteConfigREST rest(app_->options(), configs_, 3600); + ConfigFetchRequest request_data = rest.GetFetchRequestData(); + + EXPECT_EQ(request_data.client_version, 2); + // Not handling repeated package_data since spec says there's only 1. + + PackageData& package_data = request_data.package_data; + EXPECT_EQ(package_data.package_name, app_->options().package_name()); + EXPECT_EQ(package_data.gmp_project_id, app_->options().app_id()); + + // Check digests + std::map digests; + for (const auto& item : package_data.namespace_digest) { + digests[item.first] = item.second; + } + EXPECT_THAT(digests, ::testing::Eq(std::map( + {{"star_wars:droid", "DROID_DIGEST"}, + {"star_wars:starship", "STARSHIP_DIGEST"}, + {"star_wars:films", "FILMS_DIGEST"}, + {"star_wars:creatures", "CREATURES_DIGEST"}, + {"star_wars:locations", "LOCATIONS_DIGEST"}}))); + + // Check developers settings + std::map settings; + for (const auto& item : package_data.custom_variable) { + settings[item.first] = item.second; + } + EXPECT_THAT(settings, ::testing::Eq(std::map( + {{"_rcn_developer", "1"}}))); + + // The same value as in RemoteConfigRest constructor. + EXPECT_EQ(package_data.requested_cache_expiration_seconds, 3600); + + // Fetched age should be in range [7hours, 7hours + eps], + // where eps - some small value in seconds. + EXPECT_GE(package_data.fetched_config_age_seconds, 7 * 3600); + EXPECT_LE(package_data.fetched_config_age_seconds, 7 * 3600 + 10); + + // Active age should be in range [10hours, 10hours + eps], + // where eps - some small value in seconds. + EXPECT_GE(package_data.active_config_age_seconds, 10 * 3600); + EXPECT_LE(package_data.active_config_age_seconds, 10 * 3600 + 10); +} + +// Check correctness REST request setup. +TEST_F(RemoteConfigRESTTest, SetupRESTRequest) { + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.SetupRestRequest(); + + firebase::rest::RequestOptions request_options = rest.rest_request_.options(); + EXPECT_EQ(request_options.url, kServerURL); + EXPECT_EQ(request_options.method, kHTTPMethodPost); + std::string post_fields; + EXPECT_TRUE(rest.rest_request_.ReadBodyIntoString(&post_fields)); + + ConfigFetchRequest fetch_data = rest.GetFetchRequestData(); + std::string encoded_str = EncodeFetchRequest(fetch_data); + + EXPECT_EQ(GzipDecompress(post_fields), encoded_str); + EXPECT_NE(request_options.header.find("Content-Type"), + request_options.header.end()); + EXPECT_EQ(request_options.header["Content-Type"], + "application/x-protobuffer"); + EXPECT_NE(request_options.header.find("x-goog-api-client"), + request_options.header.end()); + EXPECT_THAT(request_options.header["x-goog-api-client"], + ::testing::HasSubstr("fire-cpp/")); + + // Setup a proto directly with the request data. + android::config::ConfigFetchRequest proto_data = + GetProtoFetchRequestData(rest); + std::string proto_str = proto_data.SerializeAsString(); + EXPECT_EQ(proto_str, encoded_str); + // If a proto encode doesn't match, the strings aren't easily printable, so + // the following makes it easier to examine the discrepancies. + if (encoded_str != proto_str) { + printf("--------- Encoded Proto ------------\n"); + android::config::ConfigFetchRequest proto_parse; + proto_parse.ParseFromString(encoded_str); + printf("%s\n", proto_parse.DebugString().c_str()); + printf("-------- Reference Proto -----------\n"); + printf("%s\n", proto_data.DebugString().c_str()); + printf("------------------------------------\n"); + + int max_len = (encoded_str.length() > proto_str.length()) + ? encoded_str.length() + : proto_str.length(); + printf("encoded size: %d reference size: %d\n", + static_cast(encoded_str.length()), + static_cast(proto_str.length())); + for (int i = 0; i < max_len; i++) { + char oldc = (i < proto_str.length()) ? proto_str.c_str()[i] : 0; + char newc = (i < encoded_str.length()) ? encoded_str.c_str()[i] : 0; + printf("%02X (%03d) '%c' %02X (%03d) '%c'\n", + newc, newc, newc, + oldc, oldc, oldc); + } + } +} + +// Can't pass binary body response to testing::cppsdk::ConfigSet. Can configure +// only response with not gzip body. +// +// Test passing http request to mock transport and get http +// response with error or with empty body. +// +// We have 2 different cases: +// +// 1) response code is 200. Response body is empty, because can't gunzip not +// gzip body. +// +// 2) response code is 400. Will not try gunzip body, but it's still failure, +// because response code is not 200. +TEST_F(RemoteConfigRESTTest, Fetch) { + int codes[] = {200, 400}; + for (int code : codes) { + char config[1000]; + snprintf(config, sizeof(config), + "{" + " config:[" + " {fake:'%s'," + " httpresponse: {" + " header: ['HTTP/1.1 %d Ok','Server:mock server 101']," + " body: ['some body, not proto, not gzip',]" + " }" + " }" + " ]" + "}", + kServerURL, code); + firebase::testing::cppsdk::ConfigSet(config); + + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.Fetch(*app_); + + ExpectFetchFailure(rest, code); + } +} + +TEST_F(RemoteConfigRESTTest, ParseRestResponseProtoFailure) { + std::string header = "HTTP/1.1 200 Ok"; + std::string body = GzipCompress("some fake body, NOT proto"); + + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.rest_response_.ProcessHeader(header.data(), header.length()); + rest.rest_response_.ProcessBody(body.data(), body.length()); + rest.rest_response_.MarkCompleted(); + EXPECT_EQ(rest.rest_response_.status(), 200); + + rest.ParseRestResponse(); + + ExpectFetchFailure(rest, 200); +} + +TEST_F(RemoteConfigRESTTest, ParseRestResponseSuccess) { + std::string header = "HTTP/1.1 200 Ok"; + std::string body = GzipCompress(proto_response_.SerializeAsString()); + + RemoteConfigREST rest(app_->options(), configs_, 3600); + rest.rest_response_.ProcessHeader(header.data(), header.length()); + rest.rest_response_.ProcessBody(body.data(), body.length()); + rest.rest_response_.MarkCompleted(); + EXPECT_EQ(rest.rest_response_.status(), 200); + + rest.ParseRestResponse(); + + std::map empty_map; + EXPECT_THAT(rest.fetched().config(), + ::testing::ContainerEq(NamespaceKeyValueMap({ + {"star_wars:vehicle", + {{"name", "All Terrain Armored Transport"}, + {"passengers", "40 troops"}, + {"cargo_capacity", "3,500 metric tons"}}}, + {"star_wars:droid", + {{"name", "BB-8"}, + {"height", "0.67 meters"}, + {"mass", "18 kilograms"}}}, + {"star_wars:starship", + {{"name", "Imperial I-class Star Destroyer"}, + {"length", "1,600 meters"}, + {"maximum_atmosphere_speed", "975 km/h"}}}, + {"star_wars:creatures", empty_map}, + {"star_wars:duels", empty_map}, + }))); + + EXPECT_THAT(rest.metadata().digest_by_namespace(), + ::testing::ContainerEq(MetaDigestMap( + {{"star_wars:vehicle", "VEHICLE_NEW_DIGEST"}, + {"star_wars:starship", "STARSHIP_NEW_DIGEST"}, + {"star_wars:droid", "DROID_NEW_DIGEST"}, + {"star_wars:creatures", "CREATURES_NEW_DIGEST"}, + {"star_wars:duels", "DUELS_NEW_DIGEST"}}))); + + ConfigInfo info = rest.metadata().info(); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusSuccess); + EXPECT_LE(info.fetch_time, MillisecondsSinceEpoch()); + EXPECT_GE(info.fetch_time, MillisecondsSinceEpoch() - 10000); +} + +} // namespace internal +} // namespace remote_config +} // namespace firebase diff --git a/remote_config/tests/remote_config_test.cc b/remote_config/tests/remote_config_test.cc new file mode 100644 index 0000000000..b18888ee83 --- /dev/null +++ b/remote_config/tests/remote_config_test.cc @@ -0,0 +1,692 @@ +// Copyright 2017 Google LLC +// +// 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. + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#define __ANDROID__ +#include +#include "testing/run_all_tests.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "remote_config/src/include/firebase/remote_config.h" + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#undef __ANDROID__ +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +#include +#include + +#include "testing/config.h" +#include "testing/reporter.h" +#include "testing/ticker.h" +#include "firebase/variant.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace remote_config { + +class RemoteConfigTest : public ::testing::Test { + protected: + void SetUp() override { + firebase::testing::cppsdk::TickerReset(); + firebase::testing::cppsdk::ConfigSet("{}"); + reporter_.reset(); + InitializeRemoteConfig(); + } + + void TearDown() override { + Terminate(); + delete firebase_app_; + firebase_app_ = nullptr; + + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + void InitializeRemoteConfig() { + firebase_app_ = testing::CreateApp(); + EXPECT_NE(firebase_app_, nullptr) << "Init app failed"; + + InitResult result = Initialize(*firebase_app_); + EXPECT_NE(firebase_app_, nullptr) << "Init app failed"; + EXPECT_EQ(result, kInitResultSuccess); + } + + App* firebase_app_ = nullptr; + firebase::testing::cppsdk::Reporter reporter_; +}; + +#define REPORT_EXPECT(fake, result, ...) \ + reporter_.addExpectation(fake, result, firebase::testing::cppsdk::kAny, \ + __VA_ARGS__) + +#define REPORT_EXPECT_PLATFORM(fake, result, platform, ...) \ + reporter_.addExpectation(fake, result, platform, __VA_ARGS__) + +// Check SetUp and TearDown working well. +TEST_F(RemoteConfigTest, InitializeAndTerminate) {} + +TEST_F(RemoteConfigTest, InitializeTwice) { + InitResult result = Initialize(*firebase_app_); + EXPECT_EQ(result, kInitResultSuccess); +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +TEST_F(RemoteConfigTest, SetDefaultsOnAndroid) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", {"0"}); + SetDefaults(0); +} + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +TEST_F(RemoteConfigTest, SetDefaultsWithNullConfigKeyValueVariant) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", {"{}"}); + ConfigKeyValueVariant* keyvalues = nullptr; + SetDefaults(keyvalues, 0); +} + +TEST_F(RemoteConfigTest, SetDefaultsWithConfigKeyValueVariant) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", + {"{color=black, height=120}"}); + + ConfigKeyValueVariant defaults[] = { + ConfigKeyValueVariant{"color", Variant("black")}, + ConfigKeyValueVariant{"height", Variant(120)}}; + + SetDefaults(defaults, 2); +} + +TEST_F(RemoteConfigTest, SetDefaultsWithNullConfigKeyValue) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", {"{}"}); + ConfigKeyValue* keyvalues = nullptr; + SetDefaults(keyvalues, 0); +} + +TEST_F(RemoteConfigTest, SetDefaultsWithConfigKeyValue) { + REPORT_EXPECT("FirebaseRemoteConfig.setDefaults", "", + {"{color=black, height=120, width=600.5}"}); + + ConfigKeyValue defaults[] = {ConfigKeyValue{"color", "black"}, + ConfigKeyValue{"height", "120"}, + ConfigKeyValue{"width", "600.5"}}; + + SetDefaults(defaults, 3); +} + +TEST_F(RemoteConfigTest, GetConfigSettingTrue) { + REPORT_EXPECT("FirebaseRemoteConfig.getInfo", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.isDeveloperModeEnabled", "true", + {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigSettings.isDeveloperModeEnabled'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + EXPECT_EQ(GetConfigSetting(kConfigSettingDeveloperMode), "1"); +} + +TEST_F(RemoteConfigTest, GetConfigSettingFalse) { + REPORT_EXPECT("FirebaseRemoteConfig.getInfo", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.isDeveloperModeEnabled", "false", + {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigSettings.isDeveloperModeEnabled'," + " returnvalue: {'tbool': false}}" + " ]" + "}"); + EXPECT_EQ(GetConfigSetting(kConfigSettingDeveloperMode), "0"); +} + +TEST_F(RemoteConfigTest, SetConfigSettingTrue) { + REPORT_EXPECT("FirebaseRemoteConfig.setConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled", + "", {"true"}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.build", "", {}); + SetConfigSetting(kConfigSettingDeveloperMode, "1"); +} + +TEST_F(RemoteConfigTest, SetConfigSettingFalse) { + REPORT_EXPECT("FirebaseRemoteConfig.setConfigSettings", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled", + "", {"false"}); + REPORT_EXPECT("FirebaseRemoteConfigSettings.Builder.build", "", {}); + SetConfigSetting(kConfigSettingDeveloperMode, "0"); +} + +// Start check GetBoolean functions +TEST_F(RemoteConfigTest, GetBooleanNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getBoolean", "false", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getBoolean'," + " returnvalue: {'tbool': false}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_FALSE(GetBoolean(key)); +} + +TEST_F(RemoteConfigTest, GetBooleanKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getBoolean", "true", {"give_prize"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getBoolean'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + EXPECT_TRUE(GetBoolean("give_prize")); +} + +TEST_F(RemoteConfigTest, GetBooleanKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"give_prize"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asBoolean", "true", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asBoolean'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_TRUE(GetBoolean("give_prize", info)); +} + +TEST_F(RemoteConfigTest, GetBooleanKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"give_prize"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asBoolean", "true", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asBoolean'," + " returnvalue: {'tbool': true}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_TRUE(GetBoolean("give_prize", &info)); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetBoolean functions + +// Start check GetLong functions +TEST_F(RemoteConfigTest, GetLongNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getLong", "1000", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getLong'," + " returnvalue: {'tlong': 1000}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_EQ(GetLong(key), 1000); +} + +TEST_F(RemoteConfigTest, GetLongKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getLong", "1000000000", {"price"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getLong'," + " returnvalue: {'tlong': 1000000000}}" + " ]" + "}"); + EXPECT_EQ(GetLong("price"), 1000000000); +} + +TEST_F(RemoteConfigTest, GetLongKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asLong", "100", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asLong'," + " returnvalue: {'tlong': 100}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_EQ(GetLong("wallet_cash", info), 100); +} + +TEST_F(RemoteConfigTest, GetLongKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asLong", "7000000", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asLong'," + " returnvalue: {'tlong': 7000000}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_EQ(GetLong("wallet_cash", &info), 7000000); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetLong functions + +// Start check GetDouble functions +TEST_F(RemoteConfigTest, GetDoubleNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getDouble", "1000.500", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getDouble'," + " returnvalue: {'tdouble': 1000.500}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_EQ(GetDouble(key), 1000.500); +} + +TEST_F(RemoteConfigTest, GetDoubleKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getDouble", "1000000000.000", {"price"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getDouble'," + " returnvalue: {'tdouble': 1000000000.000}}" + " ]" + "}"); + EXPECT_EQ(GetDouble("price"), 1000000000.000); +} + +TEST_F(RemoteConfigTest, GetDoubleKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asDouble", "100.999", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asDouble'," + " returnvalue: {'tdouble': 100.999}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_EQ(GetDouble("wallet_cash", info), 100.999); +} + +TEST_F(RemoteConfigTest, GetDoubleKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asDouble", "7000000.000", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asDouble'," + " returnvalue: {'tdouble': 7000000.000}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_EQ(GetDouble("wallet_cash", &info), 7000000.000); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetDouble functions + +// Start check GetString functions +TEST_F(RemoteConfigTest, GetStringNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getString", "I am fake", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getString'," + " returnvalue: {'tstring': 'I am fake'}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_EQ(GetString(key), "I am fake"); +} + +TEST_F(RemoteConfigTest, GetStringKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getString", "I am fake", {"price"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getString'," + " returnvalue: {'tstring': 'I am fake'}}" + " ]" + "}"); + EXPECT_EQ(GetString("price"), "I am fake"); +} + +TEST_F(RemoteConfigTest, GetStringKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asString", "I am fake", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asString'," + " returnvalue: {'tstring': 'I am fake'}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_EQ(GetString("wallet_cash", info), "I am fake"); +} + +TEST_F(RemoteConfigTest, GetStringKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asString", "I am fake", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asString'," + " returnvalue: {'tstring': 'I am fake'}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_EQ(GetString("wallet_cash", &info), "I am fake"); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetString functions + +// Start check GetData functions +TEST_F(RemoteConfigTest, GetDataNullKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getByteArray", "abcd", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getByteArray'," + " returnvalue: {'tstring': 'abcd'}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_THAT(GetData(key), + ::testing::Eq(std::vector({'a', 'b', 'c', 'd'}))); +} + +TEST_F(RemoteConfigTest, GetDataKey) { + REPORT_EXPECT("FirebaseRemoteConfig.getByteArray", "abc", {"name"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getByteArray'," + " returnvalue: {'tstring': 'abc'}}" + " ]" + "}"); + EXPECT_THAT(GetData("name"), + ::testing::Eq(std::vector({'a', 'b', 'c'}))); +} + +TEST_F(RemoteConfigTest, GetDataKeyAndNullInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asByteArray", "xyz", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asByteArray'," + " returnvalue: {'tstring': 'xyz'}}" + " ]" + "}"); + ValueInfo* info = nullptr; + EXPECT_THAT(GetData("wallet_cash", info), + ::testing::Eq(std::vector({'x', 'y', 'z'}))); +} + +TEST_F(RemoteConfigTest, GetDataKeyAndInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getValue", "", {"wallet_cash"}); + REPORT_EXPECT("FirebaseRemoteConfigValue.asByteArray", "xyz", {}); + REPORT_EXPECT("FirebaseRemoteConfigValue.getSource", "1", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigValue.asByteArray'," + " returnvalue: {'tstring': 'xyz'}}," + " {fake:'FirebaseRemoteConfigValue.getSource'," + " returnvalue: {'tint': 1}}" + " ]" + "}"); + ValueInfo info; + EXPECT_THAT(GetData("wallet_cash", &info), + ::testing::Eq(std::vector({'x', 'y', 'z'}))); + EXPECT_EQ(info.source, kValueSourceDefaultValue); + EXPECT_TRUE(info.conversion_successful); +} +// Finish check GetData functions + +// Start check GetKeysByPrefix functions +TEST_F(RemoteConfigTest, GetKeysByPrefix) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[1, 2, 3, 4]", + {"some_prefix"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[1, 2, 3, 4]'}}" + " ]" + "}"); + EXPECT_THAT(GetKeysByPrefix("some_prefix"), + ::testing::Eq(std::vector({"1", "2", "3", "4"}))); +} + +TEST_F(RemoteConfigTest, GetKeysByPrefixEmptyResult) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[]", {"some_prefix"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[]'}}" + " ]" + "}"); + EXPECT_THAT(GetKeysByPrefix("some_prefix"), + ::testing::Eq(std::vector({}))); +} + +TEST_F(RemoteConfigTest, GetKeysByPrefixNullPrefix) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[1, 2, 3, 4]", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[1, 2, 3, 4]'}}" + " ]" + "}"); + const char* key = nullptr; + EXPECT_THAT(GetKeysByPrefix(key), + ::testing::Eq(std::vector({"1", "2", "3", "4"}))); +} +// Finish check GetKeysByPrefix functions + +// Start check GetKeys functions +TEST_F(RemoteConfigTest, GetKeys) { + REPORT_EXPECT("FirebaseRemoteConfig.getKeysByPrefix", "[1, 2, 3, 4]", {""}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.getKeysByPrefix'," + " returnvalue: {'tstring': '[1, 2, 3, 4]'}}" + " ]" + "}"); + EXPECT_THAT(GetKeys(), + ::testing::Eq(std::vector({"1", "2", "3", "4"}))); +} +// Finish check GetKeys functions + +void Verify(const Future& result) { +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); +} + +TEST_F(RemoteConfigTest, Fetch) { + // Default value: 43200seconds = 12hours + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"43200"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Verify(Fetch()); +} + +TEST_F(RemoteConfigTest, FetchWithException) { + // Default value: 43200seconds = 12hours + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"43200"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{throwexception:true," + " exceptionmsg:'fetch failed'," + " ticker:1}}" + " ]" + "}"); + Verify(Fetch()); +} + +TEST_F(RemoteConfigTest, FetchWithExpiration) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Verify(Fetch(3600)); +} + +TEST_F(RemoteConfigTest, FetchWithExpirationAndException) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{throwexception:true," + " exceptionmsg:'fetch failed'," + " ticker:1}}" + " ]" + "}"); + Verify(Fetch(3600)); +} + +TEST_F(RemoteConfigTest, FetchLastResult) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Future result = Fetch(3600); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result.status()); + EXPECT_EQ(firebase::kFutureStatusPending, FetchLastResult().status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result.status()); + EXPECT_EQ(firebase::kFutureStatusComplete, FetchLastResult().status()); +} + +TEST_F(RemoteConfigTest, FetchLastResultWithCallFetchTwice) { + REPORT_EXPECT("FirebaseRemoteConfig.fetch", "", {"3600"}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.fetch'," + " futuregeneric:{ticker:1}}" + " ]" + "}"); + Future result1 = Fetch(3600); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result1.status()); + EXPECT_EQ(firebase::kFutureStatusPending, FetchLastResult().status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result1.status()); + EXPECT_EQ(firebase::kFutureStatusComplete, FetchLastResult().status()); + + firebase::testing::cppsdk::TickerReset(); + + Future result2 = Fetch(3600); +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusPending, result2.status()); + EXPECT_EQ(firebase::kFutureStatusPending, FetchLastResult().status()); + firebase::testing::cppsdk::TickerElapse(); +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + EXPECT_EQ(firebase::kFutureStatusComplete, result2.status()); + EXPECT_EQ(firebase::kFutureStatusComplete, FetchLastResult().status()); +} + +TEST_F(RemoteConfigTest, ActivateFetchedTrue) { + REPORT_EXPECT("FirebaseRemoteConfig.activateFetched", "true", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.activateFetched'," + " returnvalue: {'tbool': true}}" + " ]" + "}"); + EXPECT_TRUE(ActivateFetched()); +} + +TEST_F(RemoteConfigTest, ActivateFetchedFalse) { + REPORT_EXPECT("FirebaseRemoteConfig.activateFetched", "false", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfig.activateFetched'," + " returnvalue: {'tbool': false}}" + " ]" + "}"); + EXPECT_FALSE(ActivateFetched()); +} + +TEST_F(RemoteConfigTest, GetInfo) { + REPORT_EXPECT("FirebaseRemoteConfig.getInfo", "", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getFetchTimeMillis", "1000", {}); + REPORT_EXPECT("FirebaseRemoteConfigInfo.getLastFetchStatus", "2", {}); + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseRemoteConfigInfo.getFetchTimeMillis'," + " returnvalue: {'tlong': 1000}}," + " {fake:'FirebaseRemoteConfigInfo.getLastFetchStatus'," + " returnvalue: {'tint': 2}}," + " ]" + "}"); + const ConfigInfo info = GetInfo(); + EXPECT_EQ(info.fetch_time, 1000); + EXPECT_EQ(info.last_fetch_status, kLastFetchStatusFailure); + EXPECT_EQ(info.last_fetch_failure_reason, kFetchFailureReasonThrottled); +} +} // namespace remote_config +} // namespace firebase diff --git a/storage/src/common/storage_uri_parser_test.cc b/storage/src/common/storage_uri_parser_test.cc new file mode 100644 index 0000000000..53695bea6f --- /dev/null +++ b/storage/src/common/storage_uri_parser_test.cc @@ -0,0 +1,153 @@ +// Copyright 2018 Google LLC +// +// 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 "storage/src/common/storage_uri_parser.h" + +#include + +#include "app/src/include/firebase/internal/common.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace storage { +namespace internal { + +struct UriAndComponents { + // URI to parse. + const char* path; + // Expected bucket from URI. + const char* expected_bucket; + // Expected path from URI. + const char* expected_path; +}; + +TEST(StorageUriParserTest, TestInvalidUris) { + EXPECT_FALSE(UriToComponents("", "test", nullptr, nullptr)); + EXPECT_FALSE(UriToComponents("invalid://uri", "test", nullptr, nullptr)); +} + +TEST(StorageUriParserTest, TestValidUris) { + EXPECT_TRUE( + UriToComponents("gs://somebucket", "gs_scheme", nullptr, nullptr)); + EXPECT_TRUE(UriToComponents("http://domain/b/bucket", "http_scheme", nullptr, + nullptr)); + EXPECT_TRUE(UriToComponents("https://domain/b/bucket", // NOTYPO + "http_scheme", nullptr, nullptr)); +} + +// Extract components from each URI in uri_and_expected_components and compare +// with the expectedBucket & expectedPath values in the specified structure. +// object_prefix is used as a prefix for the object name supplied to each +// call to UriToComponents() to aid debugging when an error is reported by the +// method. +static void ExtractComponents( + const UriAndComponents* uri_and_expected_components, + size_t number_of_uri_and_expected_components, + const std::string& object_prefix) { + for (size_t i = 0; i < number_of_uri_and_expected_components; ++i) { + const auto& param = uri_and_expected_components[i]; + { + std::string bucket; + EXPECT_TRUE(UriToComponents( + param.path, (object_prefix + "_bucket").c_str(), &bucket, nullptr)); + EXPECT_EQ(param.expected_bucket, bucket); + } + { + std::string path; + EXPECT_TRUE(UriToComponents(param.path, (object_prefix + "_path").c_str(), + nullptr, &path)); + EXPECT_EQ(param.expected_path, path); + } + { + std::string bucket; + std::string path; + EXPECT_TRUE(UriToComponents(param.path, (object_prefix + "_all").c_str(), + &bucket, &path)); + EXPECT_EQ(param.expected_bucket, bucket); + EXPECT_EQ(param.expected_path, path); + } + } +} + +TEST(StorageUriParserTest, TestExtractGsSchemeComponents) { + const UriAndComponents kTestParams[] = { + { + "gs://somebucket", + "somebucket", + "", + }, + { + "gs://somebucket/", + "somebucket", + "", + }, + { + "gs://somebucket/a/path/to/an/object", + "somebucket", + "/a/path/to/an/object", + }, + { + "gs://somebucket/a/path/to/an/object/", + "somebucket", + "/a/path/to/an/object", + }, + }; + ExtractComponents(kTestParams, FIREBASE_ARRAYSIZE(kTestParams), "gsscheme"); +} + +TEST(StorageUriParserTest, TestExtractHttpHttpsSchemeComponents) { + const UriAndComponents kTestParams[] = { + { + "http://firebasestorage.googleapis.com/v0/b/somebucket", + "somebucket", + "", + }, + { + "http://firebasestorage.googleapis.com/v0/b/somebucket/", + "somebucket", + "", + }, + { + "http://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object", + "somebucket", + "/an/object", + }, + { + "http://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object/", + "somebucket", + "/an/object", + }, + { + "https://firebasestorage.googleapis.com/v0/b/somebucket/", + "somebucket", + "", + }, + { + "https://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object", + "somebucket", + "/an/object", + }, + { + "https://firebasestorage.googleapis.com/v0/b/somebucket/o/an/object/", + "somebucket", + "/an/object", + }, + }; + ExtractComponents(kTestParams, FIREBASE_ARRAYSIZE(kTestParams), "http(s)"); +} + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/tests/CMakeLists.txt b/storage/tests/CMakeLists.txt new file mode 100644 index 0000000000..d4ec18d6a0 --- /dev/null +++ b/storage/tests/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright 2019 Google LLC +# +# 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. + +firebase_cpp_cc_test( + firebase_storage_desktop_utils_test + SOURCES + desktop/storage_desktop_utils_tests.cc + DEPENDS + firebase_app_for_testing + firebase_rest_lib + firebase_storage + firebase_testing +) + diff --git a/storage/tests/desktop/storage_desktop_utils_tests.cc b/storage/tests/desktop/storage_desktop_utils_tests.cc new file mode 100644 index 0000000000..9726e6655e --- /dev/null +++ b/storage/tests/desktop/storage_desktop_utils_tests.cc @@ -0,0 +1,195 @@ +// Copyright 2017 Google LLC +// +// 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 + +#include "app/rest/util.h" +#include "app/src/include/firebase/app.h" +#include "app/tests/include/firebase/app_for_testing.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "storage/src/desktop/controller_desktop.h" +#include "storage/src/desktop/metadata_desktop.h" +#include "storage/src/desktop/storage_path.h" +#include "storage/src/desktop/storage_reference_desktop.h" +#include "testing/json_util.h" + +namespace { + +using firebase::App; +using firebase::storage::internal::MetadataInternal; +using firebase::storage::internal::StorageInternal; +using firebase::storage::internal::StoragePath; +using firebase::storage::internal::StorageReferenceInternal; + +// The fixture for testing helper classes for storage desktop. +class StorageDesktopUtilsTests : public ::testing::Test { + protected: + void SetUp() override { firebase::rest::util::Initialize(); } + + void TearDown() override { firebase::rest::util::Terminate(); } +}; + +// Test the GS URI-based StoragePath constructors +TEST_F(StorageDesktopUtilsTests, testGSStoragePathConstructors) { + StoragePath test_path; + + // Test basic case: + test_path = StoragePath("gs://Bucket/path/Object"); + + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/Object"); + + // Test a more complex path: + test_path = StoragePath("gs://Bucket/path/morepath/Object"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/morepath/Object"); + + // Extra slashes: + test_path = StoragePath("gs://Bucket/path////Object"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/Object"); + + // Path with no Object: + test_path = StoragePath("gs://Bucket/path////more////"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/more"); +} + +// Test the HTTP(S)-based StoragePath constructors +TEST_F(StorageDesktopUtilsTests, testHTTPStoragePathConstructors) { + StoragePath test_path; + std::string intended_bucket_result = "Bucket"; + std::string intended_path_result = "path/to/Object/Object.data"; + + // Test basic case: + test_path = StoragePath( + "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2fto%2FObject%2fObject.data"); + EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); + EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); + + // httpS (instead of http): + test_path = StoragePath( + "https://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2fto%2FObject%2fObject.data"); + EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); + EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); + + // Extra slashes: + test_path = StoragePath( + "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2f%2f%2f%2fto%2FObject%2f%2f%2f%2fObject.data"); + EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); + EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); +} + +TEST_F(StorageDesktopUtilsTests, testInvalidConstructors) { + StoragePath bad_path("argleblargle://Bucket/path1/path2/Object"); + EXPECT_FALSE(bad_path.IsValid()); +} + +// Test the StoragePath.Parent() function. +TEST_F(StorageDesktopUtilsTests, testStoragePathParent) { + StoragePath test_path; + + // Test parent, when there is an GetObject. + test_path = StoragePath("gs://Bucket/path/Object").GetParent(); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path"); + + // Test parent with no GetObject. + test_path = StoragePath("gs://Bucket/path/morepath/").GetParent(); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path"); +} + +// Test the StoragePath.Child() function. +TEST_F(StorageDesktopUtilsTests, testStoragePathChild) { + StoragePath test_path; + + // Test child when there is no object. + test_path = StoragePath("gs://Bucket/path/morepath/").GetChild("newobj"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/morepath/newobj"); + + // Test child when there is an object. + test_path = StoragePath("gs://Bucket/path/object").GetChild("newpath/"); + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path/object/newpath"); +} + +TEST_F(StorageDesktopUtilsTests, testUrlConverter) { + StoragePath test_path("gs://Bucket/path1/path2/Object"); + + EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); + EXPECT_STREQ(test_path.GetPath().c_str(), "path1/path2/Object"); + + EXPECT_STREQ(test_path.AsHttpUrl().c_str(), + "https://firebasestorage.googleapis.com" + "/v0/b/Bucket/o/path1%2Fpath2%2FObject?alt=media"); + EXPECT_STREQ(test_path.AsHttpMetadataUrl().c_str(), + "https://firebasestorage.googleapis.com" + "/v0/b/Bucket/o/path1%2Fpath2%2FObject"); +} + +TEST_F(StorageDesktopUtilsTests, testMetadataJsonExporter) { + std::unique_ptr app(firebase::testing::CreateApp()); + std::unique_ptr storage( + new StorageInternal(app.get(), "gs://abucket")); + std::unique_ptr reference( + storage->GetReferenceFromUrl("gs://abucket/path/to/a/file.txt")); + MetadataInternal metadata(reference->AsStorageReference()); + reference.reset(nullptr); + + metadata.set_cache_control("cache_control_test"); + metadata.set_content_disposition("content_disposition_test"); + metadata.set_content_encoding("content_encoding_test"); + metadata.set_content_language("content_language_test"); + metadata.set_content_type("content_type_test"); + + std::map& custom_metadata = + *metadata.custom_metadata(); + custom_metadata["key1"] = "value1"; + custom_metadata["key2"] = "value2"; + custom_metadata["key3"] = "value3"; + + std::string json = metadata.ExportAsJson(); + + // clang-format=off + EXPECT_THAT( + json, + ::firebase::testing::cppsdk::EqualsJson( + "{\"bucket\":\"abucket\"," + "\"cacheControl\":\"cache_control_test\"," + "\"contentDisposition\":\"content_disposition_test\"," + "\"contentEncoding\":\"content_encoding_test\"," + "\"contentLanguage\":\"content_language_test\"," + "\"contentType\":\"content_type_test\"," + "\"metadata\":" + "{\"key1\":\"value1\"," + "\"key2\":\"value2\"," + "\"key3\":\"value3\"}," + "\"name\":\"file.txt\"}")); + // clang-format=on +} + +} // namespace + +int main(int argc, char** argv) { + // On Linux, add: absl::SetFlag(&FLAGS_logtostderr, true); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/testing/config_test.cc b/testing/config_test.cc new file mode 100644 index 0000000000..d71280155a --- /dev/null +++ b/testing/config_test.cc @@ -0,0 +1,174 @@ +// Copyright 2020 Google +// +// 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. + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#include +#include "testing/run_all_tests.h" +#elif defined(__APPLE__) && TARGET_OS_IPHONE +#include "testing/config_ios.h" +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) +#include "testing/config_desktop.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/config.h" +#include "testing/testdata_config_generated.h" +#include "flatbuffers/idl.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +const constexpr int64_t kNullObject = -1; +const constexpr int64_t kException = -2; // NOLINT + +// Mimic what fake will do to get the test data provided by test user. +int64_t GetFutureBoolTicker(const char* fake) { + int64_t result; + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) + + // Normally, we only send test data but not read test data in C++. Android + // fakes read test data, which is in Java code. Here we use JNI calls to + // simulate that scenario. + JNIEnv* android_jni_env = GetTestJniEnv(); + jstring jfake = android_jni_env->NewStringUTF(fake); + + jclass config_cls = android_jni_env->FindClass( + "com/google/testing/ConfigAndroid"); + jobject jrow = android_jni_env->CallStaticObjectMethod( + config_cls, + android_jni_env->GetStaticMethodID( + config_cls, "get", + "(Ljava/lang/String;)Lcom/google/testing/ConfigRow;"), + jfake); + + // Catch any Java exception and thus the test itself does not die. + if (android_jni_env->ExceptionCheck()) { + android_jni_env->ExceptionDescribe(); + android_jni_env->ExceptionClear(); + result = kException; + } else if (jrow == nullptr) { + result = kNullObject; + } else { + jclass row_cls = android_jni_env->FindClass( + "com/google/testing/ConfigRow"); + jobject jfuturebool = android_jni_env->CallObjectMethod( + jrow, android_jni_env->GetMethodID( + row_cls, "futurebool", + "()Lcom/google/testing/FutureBool;")); + EXPECT_EQ(android_jni_env->ExceptionCheck(), JNI_FALSE); + android_jni_env->ExceptionClear(); + jclass futurebool_cls = android_jni_env->FindClass( + "com/google/testing/FutureBool"); + jlong jticker = android_jni_env->CallLongMethod( + jfuturebool, + android_jni_env->GetMethodID(futurebool_cls, "ticker", "()J")); + EXPECT_EQ(android_jni_env->ExceptionCheck(), JNI_FALSE); + android_jni_env->ExceptionClear(); + + android_jni_env->DeleteLocalRef(futurebool_cls); + android_jni_env->DeleteLocalRef(jfuturebool); + android_jni_env->DeleteLocalRef(row_cls); + android_jni_env->DeleteLocalRef(jrow); + result = jticker; + } + android_jni_env->DeleteLocalRef(config_cls); + android_jni_env->DeleteLocalRef(jfake); + +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP) + + const ConfigRow* config = ConfigGet(fake); + if (config == nullptr) { + result = kNullObject; + } else { + EXPECT_EQ(fake, config->fake()->str()); + result = config->futurebool()->ticker(); + } + +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + + return result; +} + +// Verify fake gets the data set by test user. +TEST(ConfigTest, TestConfigSetAndGet) { + ConfigSet( + "{" + " config:[" + " {fake:'key'," + " futurebool:{value:Error,ticker:10}}" + " ]" + "}"); + EXPECT_EQ(10, GetFutureBoolTicker("key")); +} + +// Verify fake gets provided data for multiple fake case. +TEST(ConfigTest, TestConfigSetMultipleAndGet) { + ConfigSet( + "{" + " config:[" + " {fake:'1',futurebool:{ticker:1}}," + " {fake:'7',futurebool:{ticker:7}}," + " {fake:'2',futurebool:{ticker:2}}," + " {fake:'6',futurebool:{ticker:6}}," + " {fake:'3',futurebool:{ticker:3}}," + " {fake:'5',futurebool:{ticker:5}}," + " {fake:'4',futurebool:{ticker:4}}" + " ]" + "}"); + char fake[] = {0, 0}; + for (int i = 1; i <= 7; ++i) { + fake[0] = '0' + i; + EXPECT_EQ(i, GetFutureBoolTicker(fake)); + } +} + +// Verify fake gets null if it is not specified by test user. +TEST(ConfigTest, TestConfigSetAndGetNothing) { + ConfigSet( + "{" + " config:[" + " {fake:'key'," + " futurebool:{value:False,ticker:10}}" + " ]" + "}"); + EXPECT_EQ(kNullObject, GetFutureBoolTicker("absence")); +} + +// Test the reset of test config. Nothing to verify except to make sure code +// nothing is not broken. +TEST(ConfigTest, TestConfigReset) { + ConfigSet("{}"); + ConfigReset(); +} + +// Verify exception raises when access the unset config. +TEST(ConfigDeathTest, TestConfigResetAndGet) { + ConfigSet("{}"); + ConfigReset(); +// Somehow the death test does not work on android emulator nor ios emulator. +#if !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE) + EXPECT_DEATH(GetFutureBoolTicker("absence"), ""); +#endif // !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE) +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/reporter_impl_fake.cc b/testing/reporter_impl_fake.cc new file mode 100644 index 0000000000..e8e125120f --- /dev/null +++ b/testing/reporter_impl_fake.cc @@ -0,0 +1,34 @@ +// Copyright 2020 Google +// +// 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 "testing/reporter_impl_fake.h" + +#include "testing/reporter_impl.h" + +namespace firebase { +namespace testing { +namespace cppsdk { +namespace fake { + +void TestFunction() { + FakeReporter->AddReport( + "fake_function_name", "fake_function_result", + std::initializer_list({ + "fake_argument0", "fake_argument1", "fake_argument2"})); +} + +} // namespace fake +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/reporter_impl_test.cc b/testing/reporter_impl_test.cc new file mode 100644 index 0000000000..bce4496651 --- /dev/null +++ b/testing/reporter_impl_test.cc @@ -0,0 +1,45 @@ +// Copyright 2020 Google +// +// 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 "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/reporter.h" +#include "testing/reporter_impl_fake.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +class ReporterImplTest : public ::testing::Test { + protected: + void SetUp() override { reporter_.reset(); } + + void TearDown() override { + EXPECT_THAT(reporter_.getFakeReports(), + ::testing::Eq(reporter_.getExpectations())); + } + + Reporter reporter_; +}; + +TEST_F(ReporterImplTest, Test) { + reporter_.addExpectation( + "fake_function_name", "fake_function_result", kAny, + {"fake_argument0", "fake_argument1", "fake_argument2"}); + fake::TestFunction(); +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/reporter_test.cc b/testing/reporter_test.cc new file mode 100644 index 0000000000..0a87863113 --- /dev/null +++ b/testing/reporter_test.cc @@ -0,0 +1,190 @@ +// Copyright 2020 Google +// +// 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 + +#include "testing/reporter.h" +#include "testing/run_all_tests.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +TEST(ReportRowTest, TestGetFake) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getFake(), "fake"); +} + +TEST(ReportRowTest, TestGetResult) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getResult(), "result"); +} + +TEST(ReportRowTest, TestGetArgs) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_THAT(r.getArgs(), + ::testing::Eq(std::vector{"1", "2", "3"})); +} + +TEST(ReportRowTest, TestGetPlatform) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kAny); + + r = ReportRow("fake", "result", kAndroid, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kAndroid); + + r = ReportRow("fake", "result", kIos, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kIos); + + r = ReportRow("fake", "result", {"1", "2", "3"}); + EXPECT_EQ(r.getPlatform(), kAny); +} + +TEST(ReportRowTest, TestGetPlatformString) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "any"); + + r = ReportRow("fake", "result", kAndroid, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "android"); + + r = ReportRow("fake", "result", kIos, {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "ios"); + + r = ReportRow("fake", "result", {"1", "2", "3"}); + EXPECT_EQ(r.getPlatformString(), "any"); +} + +TEST(ReportRowTest, TestToString) { + ReportRow r("fake", "result", kAny, {"1", "2", "3"}); + EXPECT_EQ(r.toString(), "fake result any [1 2 3]"); + + r = ReportRow("", "", kAny, {}); + EXPECT_EQ(r.toString(), " any []"); +} + +// Compare only fake_ values +TEST(ReportRowTest, TestLessThanOperator) { + ReportRow r1("abc", "9876", kAny, {"a", "a", "a"}); + ReportRow r2("xyz", "5555", kAny, {"x", "x", "x"}); + + EXPECT_TRUE(r1 < r2); + EXPECT_FALSE(r2 < r1); + + EXPECT_FALSE(r1 < r1); + EXPECT_FALSE(r2 < r2); +} + +TEST(ReportRowTest, TestEqualOperator) { + ReportRow r1("abc", "9876", kAny, {"a", "a", "a"}); + ReportRow r2("xyz", "5555", kAny, {"x", "x", "x"}); + ReportRow r3("xyz", "4444", kAny, {"z", "z", "z"}); + + EXPECT_FALSE(r1 == r2); + EXPECT_FALSE(r2 == r1); + + EXPECT_TRUE(r1 == r1); + EXPECT_TRUE(r2 == r2); + + EXPECT_FALSE(r2 == r3); +} + +TEST(ReportRowTest, TestNotEqualOperator) { + ReportRow r1("abc", "9876", kAny, {"a", "a", "a"}); + ReportRow r2("xyz", "5555", kAny, {"x", "x", "x"}); + ReportRow r3("xyz", "4444", kAny, {"z", "z", "z"}); + + EXPECT_TRUE(r1 != r2); + EXPECT_TRUE(r2 != r1); + + EXPECT_FALSE(r1 != r1); + EXPECT_FALSE(r2 != r2); + + EXPECT_TRUE(r2 != r3); +} + +class ReporterTest : public ::testing::Test { + protected: + Reporter r_; +}; + +TEST_F(ReporterTest, TestGetExpectations) { + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + r_.addExpectation("fake2", "result2", kAny, {"one", "two"}); + r_.addExpectation(ReportRow("fake3", "result3", kAny, {"one", "two"})); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"}), + ReportRow("fake2", "result2", kAny, {"one", "two"}), + ReportRow("fake3", "result3", kAny, {"one", "two"})})); +} + +TEST_F(ReporterTest, TestGetExpectationsSortedByKey) { + r_.addExpectation(ReportRow("fake3", "result3", kAny, {"one", "two"})); + r_.addExpectation("fake2", "result2", kAny, {"one", "two"}); + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"}), + ReportRow("fake2", "result2", kAny, {"one", "two"}), + ReportRow("fake3", "result3", kAny, {"one", "two"})})); +} + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) || defined(__ANDROID__) +TEST_F(ReporterTest, TestGetExpectationsAndroid) { + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + r_.addExpectation("fake2", "result2", kAndroid, {"one", "two"}); + r_.addExpectation(ReportRow("fake3", "result3", kIos, {"one", "two"})); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"}), + ReportRow("fake2", "result2", kAndroid, {"one", "two"})})); +} + +TEST_F(ReporterTest, TestResetAndroid) { + r_.addExpectation("fake1", "result1", kAny, {"one", "two"}); + + EXPECT_THAT(r_.getExpectations(), + ::testing::Eq(std::vector{ + ReportRow("fake1", "result1", kAny, {"one", "two"})})); + r_.reset(); + EXPECT_THAT(r_.getExpectations(), ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetFakeReportsAndroid) { + EXPECT_THAT(r_.getFakeReports(), ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetAllFakesAndroid) { + EXPECT_THAT(r_.getAllFakes(), ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetFakeArgsAndroid) { + EXPECT_THAT(r_.getFakeArgs("some_fake"), + ::testing::Eq(std::vector{})); +} + +TEST_F(ReporterTest, TestGetFakeResultAndroid) { + EXPECT_EQ(r_.getFakeResult("some_fake"), ""); +} +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) || defined(__ANDROID__) + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/ticker_test.cc b/testing/ticker_test.cc new file mode 100644 index 0000000000..e6c6238ea5 --- /dev/null +++ b/testing/ticker_test.cc @@ -0,0 +1,170 @@ +// Copyright 2020 Google +// +// 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. + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif + +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +#include +#include "testing/run_all_tests.h" +#elif defined(__APPLE__) && TARGET_OS_IPHONE +#include "testing/ticker_ios.h" +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) +#include "testing/ticker_desktop.h" +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP), defined(__APPLE__) + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "testing/ticker.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +// We count the status change by this. +static int g_status_count = 0; + +// Now we define example ticker class. TickerObserver is abstract and we cannot +// test it directly. Generally speaking, fakes mimic callbacks by inheriting +// TickerObserver class and overriding Update() method. +#if defined(FIREBASE_ANDROID_FOR_DESKTOP) +extern "C" JNIEXPORT void JNICALL +Java_com_google_firebase_testing_cppsdk_TickerExample_nativeFunction( + JNIEnv* env, jobject this_obj, jlong ticker, jlong delay) { + if (ticker == delay) { + ++g_status_count; + } +} + +class Tickers { + public: + Tickers(std::initializer_list delays) { + JNIEnv* android_jni_env = GetTestJniEnv(); + jclass class_obj = android_jni_env->FindClass( + "com/google/testing/TickerExample"); + jmethodID methid_id = + android_jni_env->GetMethodID(class_obj, "", "(J)V"); + for (int64_t delay : delays) { + jobject observer = + android_jni_env->NewObject(class_obj, methid_id, delay); + android_jni_env->DeleteLocalRef(observer); + } + android_jni_env->DeleteLocalRef(class_obj); + } +}; +#else // defined(FIREBASE_ANDROID_FOR_DESKTOP) +class TickerExample : public TickerObserver { + public: + explicit TickerExample(int64_t delay) : delay_(delay) { + RegisterTicker(this); + } + + void Elapse() override { + if (TickerNow() == delay_) { + ++g_status_count; + } + } + + private: + // When the callback should happen. + const int64_t delay_; +}; + +class Tickers { + public: + Tickers(std::initializer_list delays) { + for (int64_t delay : delays) { + tickers_.push_back( + std::shared_ptr(new TickerExample(delay))); + } + } + + private: + std::vector > tickers_; +}; +#endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) + +class TickerTest : public ::testing::Test { + protected: + void SetUp() override { g_status_count = 0; } + void TearDown() override { TickerReset(); } +}; + +// This test make sure nothing is broken by calling a sequence of elapse and +// reset. Since there is no observer, we do not have anything to verify yet. +TEST_F(TickerTest, TestNoObserver) { + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + + TickerReset(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); +} + +// Test one observer that changes status immediately. +TEST_F(TickerTest, TestObserverCallbackImmediate) { + Tickers tickers({0L}); + + // Now verify the status changed immediately. + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); +} + +// Test one observer that changes status after two tickers. +TEST_F(TickerTest, TestObserverDelayTwo) { + Tickers tickers({2L}); + + // Now start the ticker and verify. + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); +} + +// Test two observers that changes status after one, respectively, two tickers. +TEST_F(TickerTest, TestMultipleObservers) { + Tickers tickers({1L, 2L}); + + // Now start the ticker and verify. + EXPECT_EQ(0, g_status_count); + TickerElapse(); + EXPECT_EQ(1, g_status_count); + TickerElapse(); + EXPECT_EQ(2, g_status_count); + TickerElapse(); + EXPECT_EQ(2, g_status_count); +} +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/util_android_test.cc b/testing/util_android_test.cc new file mode 100644 index 0000000000..6314868576 --- /dev/null +++ b/testing/util_android_test.cc @@ -0,0 +1,86 @@ +// Copyright 2020 Google +// +// 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 +#include + +#include "testing/run_all_tests.h" +#include "testing/util_android.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +class UtilTest : public ::testing::Test { + protected: + JNIEnv* env_ = GetTestJniEnv(); +}; + +TEST_F(UtilTest, JavaStringToString) { + jstring java_string = env_->NewStringUTF("hello world"); + std::string cc_string = util::JavaStringToStdString(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_EQ(cc_string, "hello world"); +} + +TEST_F(UtilTest, JavaStringToStringWithEmptyJavaString) { + jstring java_string = env_->NewStringUTF(nullptr); + std::string cc_string = util::JavaStringToStdString(env_, java_string); + env_->DeleteLocalRef(java_string); + EXPECT_EQ(cc_string, ""); +} + +TEST_F(UtilTest, JavaStringListToStdStringVector) { + std::vector arr = {"one", "two", "three", "four", "five"}; + + jclass jarray_list_class = env_->FindClass("java/util/ArrayList"); + jobject jarray_list = env_->NewObject( + jarray_list_class, env_->GetMethodID(jarray_list_class, "", "()V")); + + for (const std::string& s : arr) { + jstring java_string = env_->NewStringUTF(s.c_str()); + env_->CallBooleanMethod( + jarray_list, + env_->GetMethodID(jarray_list_class, "add", "(Ljava/lang/Object;)Z"), + java_string); + util::CheckAndClearException(env_); + env_->DeleteLocalRef(java_string); + } + + EXPECT_THAT(util::JavaStringListToStdStringVector(env_, jarray_list), + ::testing::Eq(arr)); + + env_->DeleteLocalRef(jarray_list_class); + env_->DeleteLocalRef(jarray_list); +} + +TEST_F(UtilTest, JavaStringListToStdStringVectorWithEmptyJavaList) { + jclass jarray_list_class = env_->FindClass("java/util/ArrayList"); + jobject jarray_list = env_->NewObject( + jarray_list_class, env_->GetMethodID(jarray_list_class, "", "()V")); + + EXPECT_THAT(util::JavaStringListToStdStringVector(env_, jarray_list), + ::testing::Eq(std::vector())); + + env_->DeleteLocalRef(jarray_list_class); + env_->DeleteLocalRef(jarray_list); +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/testing/util_ios_test.mm b/testing/util_ios_test.mm new file mode 100644 index 0000000000..05022f5a3a --- /dev/null +++ b/testing/util_ios_test.mm @@ -0,0 +1,80 @@ +// Copyright 2020 Google +// +// 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. + +#import + +#include + +#include "testing/config.h" +#include "testing/ticker.h" +#include "testing/util_ios.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace testing { +namespace cppsdk { + +TEST(TickerTest, TestCallbackTicker) { + TickerReset(); + ConfigSet( + "{" + " config:[" + " {fake:'a',futuregeneric:{throwexception:false,ticker:1}}," + " {fake:'b',futuregeneric:{throwexception:true,exceptionmsg:'failed',ticker:2}}," + " {fake:'c',futuregeneric:{throwexception:false,ticker:3}}," + " {fake:'d',futuregeneric:{throwexception:true,exceptionmsg:'failed',ticker:4}}" + " ]" + "}"); + + __block int count = 0; + // Now we create four fake objects on the fly; all are managed by manager. + CallbackTickerManager manager; + // Without param. + manager.Add(@"a", ^(NSError* _Nullable error) { if (!error) count++; }); + manager.Add(@"b", ^(NSError* _Nullable error) { if (!error) count++; }); + // With param. + manager.Add(@"c", ^(NSString* param, NSError* _Nullable error) { if (!error) count++; }, @"par"); + manager.Add(@"d", ^(NSString* param, NSError* _Nullable error) { if (!error) count++; }, @"par"); + + // nothing happens so far. + EXPECT_EQ(0, count); + + // a succeeds and increases counter. + TickerElapse(); + EXPECT_EQ(1, count); + + // b fails. + TickerElapse(); + EXPECT_EQ(1, count); + + // c succeeds and increases counter. + TickerElapse(); + EXPECT_EQ(2, count); + + // d fails. + TickerElapse(); + EXPECT_EQ(2, count); + + // nothing happens afterwards. + TickerElapse(); + EXPECT_EQ(2, count); + + TickerReset(); + ConfigReset(); +} + +} // namespace cppsdk +} // namespace testing +} // namespace firebase diff --git a/version_header_test.py b/version_header_test.py new file mode 100644 index 0000000000..82e7ce4d91 --- /dev/null +++ b/version_header_test.py @@ -0,0 +1,103 @@ +# Copyright 2018 Google LLC +# +# 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. + +"""Tests for google3.firebase.app.client.cpp.version_header.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from google3.testing.pybase import googletest +from google3.firebase.app.client.cpp import version_header + +EXPECTED_VERSION_HEADER = r"""// Copyright 2016 Google Inc. All Rights Reserved. + +#ifndef FIREBASE_APP_CLIENT_CPP_SRC_VERSION_H_ +#define FIREBASE_APP_CLIENT_CPP_SRC_VERSION_H_ + +/// @def FIREBASE_VERSION_MAJOR +/// @brief Major version number of the Firebase C++ SDK. +/// @see kFirebaseVersionString +#define FIREBASE_VERSION_MAJOR 1 +/// @def FIREBASE_VERSION_MINOR +/// @brief Minor version number of the Firebase C++ SDK. +/// @see kFirebaseVersionString +#define FIREBASE_VERSION_MINOR 2 +/// @def FIREBASE_VERSION_REVISION +/// @brief Revision number of the Firebase C++ SDK. +/// @see kFirebaseVersionString +#define FIREBASE_VERSION_REVISION 3 + +/// @cond FIREBASE_APP_INTERNAL +#define FIREBASE_STRING_EXPAND(X) #X +#define FIREBASE_STRING(X) FIREBASE_STRING_EXPAND(X) +/// @endcond + +// Version number. +// clang-format off +#define FIREBASE_VERSION_NUMBER_STRING \ + FIREBASE_STRING(FIREBASE_VERSION_MAJOR) "." \ + FIREBASE_STRING(FIREBASE_VERSION_MINOR) "." \ + FIREBASE_STRING(FIREBASE_VERSION_REVISION) +// clang-format on + +// Identifier for version string, e.g. kFirebaseVersionString. +#define FIREBASE_VERSION_IDENTIFIER(library) k##library##VersionString + +// Concatenated version string, e.g. "Firebase C++ x.y.z". +#define FIREBASE_VERSION_STRING(library) \ + #library " C++ " FIREBASE_VERSION_NUMBER_STRING + +#if !defined(DOXYGEN) +#if !defined(_WIN32) && !defined(__CYGWIN__) +#define DEFINE_FIREBASE_VERSION_STRING(library) \ + extern volatile __attribute__((weak)) \ + const char* FIREBASE_VERSION_IDENTIFIER(library); \ + volatile __attribute__((weak)) \ + const char* FIREBASE_VERSION_IDENTIFIER(library) = \ + FIREBASE_VERSION_STRING(library) +#else +#define DEFINE_FIREBASE_VERSION_STRING(library) \ + static const char* FIREBASE_VERSION_IDENTIFIER(library) = \ + FIREBASE_VERSION_STRING(library) +#endif // !defined(_WIN32) && !defined(__CYGWIN__) +#else // if defined(DOXYGEN) + +/// @brief Namespace that encompasses all Firebase APIs. +namespace firebase { + +/// @brief String which identifies the current version of the Firebase C++ +/// SDK. +/// +/// @see FIREBASE_VERSION_MAJOR +/// @see FIREBASE_VERSION_MINOR +/// @see FIREBASE_VERSION_REVISION +static const char* kFirebaseVersionString = FIREBASE_VERSION_STRING; + +} // namespace firebase +#endif // !defined(DOXYGEN) + +#endif // FIREBASE_APP_CLIENT_CPP_SRC_VERSION_H_ +""" + + +class VersionHeaderGeneratorTest(googletest.TestCase): + + def test_generate_header(self): + result_header = version_header.generate_header(1, 2, 3) + self.assertEqual(result_header, EXPECTED_VERSION_HEADER) + + +if __name__ == '__main__': + googletest.main() From aed558eafdb2f7c095c7ae8e2f43d64b25e9c3f9 Mon Sep 17 00:00:00 2001 From: Jon Simantov Date: Mon, 20 Jul 2020 15:00:09 -0700 Subject: [PATCH 007/109] Add empty workflow to manually trigger. --- .github/workflows/cpp-packaging.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/cpp-packaging.yml diff --git a/.github/workflows/cpp-packaging.yml b/.github/workflows/cpp-packaging.yml new file mode 100644 index 0000000000..038ae29a41 --- /dev/null +++ b/.github/workflows/cpp-packaging.yml @@ -0,0 +1,14 @@ +name: C++ binary SDK packaging + +on: + workflow_dispatch: + inputs: + commitId: + description: 'commit ID to package' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: noop + run: true From 67642c84832ea06e01a2c149e16ec44e715716d5 Mon Sep 17 00:00:00 2001 From: wuandy Date: Thu, 18 Jun 2020 12:15:17 -0700 Subject: [PATCH 008/109] Add a user callback executor for android. Also fixes a flaky test because assertion is some times done before expected remote event arrives. PiperOrigin-RevId: 317154095 --- .../src/android/document_reference_android.cc | 5 ++- firestore/src/android/firestore_android.cc | 42 +++++++++++++++++-- firestore/src/android/firestore_android.h | 6 +++ firestore/src/android/query_android.cc | 4 +- firestore/src/android/query_android.h | 3 +- firestore/src/tests/validation_test.cc | 2 +- .../SilentRejectionSingleThreadExecutor.java | 33 +++++++++++++++ 7 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc index 226e1e7c12..71afb8cf80 100644 --- a/firestore/src/android/document_reference_android.cc +++ b/firestore/src/android/document_reference_android.cc @@ -40,7 +40,8 @@ namespace firestore { "[Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;"), \ X(Delete, "delete", "()Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotListener, "addSnapshotListener", \ - "(Lcom/google/firebase/firestore/MetadataChanges;" \ + "(Ljava/util/concurrent/Executor;" \ + "Lcom/google/firebase/firestore/MetadataChanges;" \ "Lcom/google/firebase/firestore/EventListener;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on @@ -246,7 +247,7 @@ ListenerRegistration DocumentReferenceInternal::AddSnapshotListener( jobject java_registration = env->CallObjectMethod( obj_, document_reference::GetMethodId(document_reference::kAddSnapshotListener), - java_metadata, java_listener); + firestore_->user_callback_executor(), java_metadata, java_listener); env->DeleteLocalRef(java_listener); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index 2fbafec34a..e0509f6bc2 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -81,7 +81,7 @@ const char kApiIdentifier[] = "Firestore"; X(ClearPersistence, "clearPersistence", \ "()Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotsInSyncListener, "addSnapshotsInSyncListener", \ - "(Ljava/lang/Runnable;)" \ + "(Ljava/util/concurrent/Executor;Ljava/lang/Runnable;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on @@ -92,6 +92,15 @@ METHOD_LOOKUP_DEFINITION(firebase_firestore, "com/google/firebase/firestore/FirebaseFirestore", FIREBASE_FIRESTORE_METHODS) +#define SILENT_REJECTION_EXECUTOR_METHODS(X) X(Constructor, "", "()V") +METHOD_LOOKUP_DECLARATION(silent_rejection_executor, + SILENT_REJECTION_EXECUTOR_METHODS) +METHOD_LOOKUP_DEFINITION(silent_rejection_executor, + PROGUARD_KEEP_CLASS + "com/google/firebase/firestore/internal/cpp/" + "SilentRejectionSingleThreadExecutor", + SILENT_REJECTION_EXECUTOR_METHODS) + Mutex FirestoreInternal::init_mutex_; // NOLINT int FirestoreInternal::initialize_count_ = 0; @@ -118,6 +127,16 @@ FirestoreInternal::FirestoreInternal(App* app) { // default, we may safely remove the calls below. set_settings(settings()); + jobject user_callback_executor_obj = + env->NewObject(silent_rejection_executor::GetClass(), + silent_rejection_executor::GetMethodId( + silent_rejection_executor::kConstructor)); + + CheckAndClearJniExceptions(env); + FIREBASE_ASSERT(user_callback_executor_obj != nullptr); + user_callback_executor_ = env->NewGlobalRef(user_callback_executor_obj); + env->DeleteLocalRef(user_callback_executor_obj); + future_manager_.AllocFutureApi(this, static_cast(FirestoreFn::kCount)); } @@ -179,13 +198,17 @@ bool FirestoreInternal::InitializeEmbeddedClasses(App* app) { ::firebase_firestore::firestore_resources_size)); return EventListenerInternal::InitializeEmbeddedClasses(app, &embedded_files) && - TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files); + TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files) && + silent_rejection_executor::CacheClassFromFiles(env, activity, + &embedded_files) && + silent_rejection_executor::CacheMethodIds(env, activity); } /* static */ void FirestoreInternal::ReleaseClasses(App* app) { JNIEnv* env = app->GetJNIEnv(); firebase_firestore::ReleaseClass(env); + silent_rejection_executor::ReleaseClass(env); util::CheckAndClearJniExceptions(env); // Call Terminate on each Firestore internal class. @@ -226,6 +249,13 @@ void FirestoreInternal::Terminate(App* app) { } } +void FirestoreInternal::ShutdownUserCallbackExecutor() { + JNIEnv* env = app_->GetJNIEnv(); + auto shutdown_method = env->GetMethodID( + env->GetObjectClass(user_callback_executor_), "shutdown", "()V"); + env->CallVoidMethod(user_callback_executor_, shutdown_method); +} + FirestoreInternal::~FirestoreInternal() { // If initialization failed, there is nothing to clean up. if (app_ == nullptr) return; @@ -243,7 +273,11 @@ FirestoreInternal::~FirestoreInternal() { future_manager_.ReleaseFutureApi(this); + ShutdownUserCallbackExecutor(); + JNIEnv* env = app_->GetJNIEnv(); + env->DeleteGlobalRef(user_callback_executor_); + user_callback_executor_ = nullptr; env->DeleteGlobalRef(obj_); obj_ = nullptr; Terminate(app_); @@ -419,6 +453,7 @@ Future FirestoreInternal::EnableNetworkLastResult() { Future FirestoreInternal::Terminate() { JNIEnv* env = app_->GetJNIEnv(); + jobject task = env->CallObjectMethod( obj_, firebase_firestore::GetMethodId(firebase_firestore::kTerminate)); CheckAndClearJniExceptions(env); @@ -427,6 +462,7 @@ Future FirestoreInternal::Terminate() { promise.RegisterForTask(FirestoreFn::kTerminate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); + return promise.GetFuture(); } @@ -483,7 +519,7 @@ ListenerRegistration FirestoreInternal::AddSnapshotsInSyncListener( obj_, firebase_firestore::GetMethodId( firebase_firestore::kAddSnapshotsInSyncListener), - java_runnable); + user_callback_executor(), java_runnable); env->DeleteLocalRef(java_runnable); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h index e2533f39e1..9c4cb6b536 100644 --- a/firestore/src/android/firestore_android.h +++ b/firestore/src/android/firestore_android.h @@ -151,6 +151,8 @@ class FirestoreInternal { Firestore* firestore_public() { return firestore_public_; } const Firestore* firestore_public() const { return firestore_public_; } + jobject user_callback_executor() const { return user_callback_executor_; } + private: // Gets the reference-counted Future implementation of this instance, which // can be used to create a Future. @@ -163,6 +165,8 @@ class FirestoreInternal { return static_cast&>(result); } + void ShutdownUserCallbackExecutor(); + static bool Initialize(App* app); static void ReleaseClasses(App* app); static void Terminate(App* app); @@ -173,6 +177,8 @@ class FirestoreInternal { static Mutex init_mutex_; static int initialize_count_; + jobject user_callback_executor_; + App* app_ = nullptr; Firestore* firestore_public_ = nullptr; // Java Firestore global ref. diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index be33c299f2..7e858406d9 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -205,8 +205,8 @@ ListenerRegistration QueryInternal::AddSnapshotListener( // Register listener. jobject java_registration = env->CallObjectMethod( - obj_, query::GetMethodId(query::kAddSnapshotListener), java_metadata, - java_listener); + obj_, query::GetMethodId(query::kAddSnapshotListener), + firestore_->user_callback_executor(), java_metadata, java_listener); env->DeleteLocalRef(java_listener); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h index 8c9d089a90..9ce3243db0 100644 --- a/firestore/src/android/query_android.h +++ b/firestore/src/android/query_android.h @@ -87,7 +87,8 @@ enum class QueryFn { "(Lcom/google/firebase/firestore/Source;)" \ "Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotListener, "addSnapshotListener", \ - "(Lcom/google/firebase/firestore/MetadataChanges;" \ + "(Ljava/util/concurrent/Executor;" \ + "Lcom/google/firebase/firestore/MetadataChanges;" \ "Lcom/google/firebase/firestore/EventListener;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on diff --git a/firestore/src/tests/validation_test.cc b/firestore/src/tests/validation_test.cc index 2cb243a194..61be692f37 100644 --- a/firestore/src/tests/validation_test.cc +++ b/firestore/src/tests/validation_test.cc @@ -670,7 +670,7 @@ TEST_F(ValidationTest, Await(firestore()->EnableNetwork()); Await(future); - snapshot = accumulator.Await(); + snapshot = accumulator.AwaitRemoteEvent(); EXPECT_FALSE(snapshot.metadata().has_pending_writes()); EXPECT_NO_THROW(collection.OrderBy(FieldPath({"timestamp"})) .EndAt(snapshot.documents().at(0)) diff --git a/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java b/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java new file mode 100644 index 0000000000..e66defc7b3 --- /dev/null +++ b/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java @@ -0,0 +1,33 @@ +package com.google.firebase.firestore.internal.cpp; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +/** + * Simple {@code Executor} implementation wraps around a single threaded executor and swallows + * {@code RejectedExecutionException} when executing commands. + * + *

    During shutdown, the C++ API must be able to prevent user callbacks from running after the + * Firestore object has been disposed. To do so, it shuts down its executors, accepting that new + * callbacks may be rejected. This class catches and discards the {@code RejectedExecutionException} + * that is thrown by the underlying Java executor after shutdown, bridging the gap between C++ + * expectations and the Java implementation. + */ +public final class SilentRejectionSingleThreadExecutor implements Executor { + private final ExecutorService internalExecutor = Executors.newSingleThreadExecutor(); + + @Override + public void execute(Runnable command) { + try { + internalExecutor.execute(command); + } catch (RejectedExecutionException e) { + // Swallow RejectedExecutionException + } + } + + public void shutdown() { + internalExecutor.shutdown(); + } +} From f9edb56220d83e696bf766bfd758211cfd28f3df Mon Sep 17 00:00:00 2001 From: Googler Date: Fri, 19 Jun 2020 16:19:35 -0700 Subject: [PATCH 009/109] Migrating deprecated aliases for absl::StrSplit, and the corresponding predicates. See b/158478280 and go/absl-cleanup-lsc for more details. This change was produced using rename_function with the spec: rename { rename_spec { new_header: "third_party/absl/strings/str_split.h" old_name: "strings::Split" new_name: "absl::StrSplit" } } Additionally, a global find/replace was done on: strings::AllowEmpty -> absl::AllowEmpty strings::SkipEmpty -> absl::SkipEmpty strings::SkipWhitespace -> absl::SkipWhitespace Tested: TAP sample presubmit queue http://test/OCL:317011231:BASE:317041434:1592460837225:fc71f604 PiperOrigin-RevId: 317399788 --- app/rest/tests/zlibwrapper_unittest.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/rest/tests/zlibwrapper_unittest.cc b/app/rest/tests/zlibwrapper_unittest.cc index 9d8e31d62f..d7ee8398ec 100644 --- a/app/rest/tests/zlibwrapper_unittest.cc +++ b/app/rest/tests/zlibwrapper_unittest.cc @@ -891,7 +891,7 @@ TEST_P(ZLibWrapperTest, ChunkedCompression) { TestGzip(&zlib, uncompbuf); } -// Simple helper to force specialization of strings::Split. +// Simple helper to force specialization of absl::StrSplit. std::vector GetFilesToProcess() { std::string files_to_process = FLAGS_files_to_process.empty() From 8fdca57169a64c463a7531353f03b5273cda2354 Mon Sep 17 00:00:00 2001 From: amablue Date: Fri, 19 Jun 2020 16:34:53 -0700 Subject: [PATCH 010/109] Incremented version numbers to 6.15.1 PiperOrigin-RevId: 317402209 --- cpp_sdk_version.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp_sdk_version.json b/cpp_sdk_version.json index 638d964fba..5f4404eb40 100644 --- a/cpp_sdk_version.json +++ b/cpp_sdk_version.json @@ -1,5 +1,5 @@ { - "released": "6.15.0", - "stable": "6.15.0", - "head": "6.15.0" + "released": "6.15.1", + "stable": "6.15.1", + "head": "6.15.1" } From 7b5cc3c8e5c61dddf6c76c5c70000b0381d5d2ae Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 22 Jun 2020 14:31:28 -0700 Subject: [PATCH 011/109] Automated g4 rollback of changelist 317154095. *** Reason for rollback *** Not ready for release yet *** Original change description *** Add a user callback executor for android. Also fixes a flaky test because assertion is some times done before expected remote event arrives. *** PiperOrigin-RevId: 317736475 --- .../src/android/document_reference_android.cc | 5 +-- firestore/src/android/firestore_android.cc | 42 ++----------------- firestore/src/android/firestore_android.h | 6 --- firestore/src/android/query_android.cc | 4 +- firestore/src/android/query_android.h | 3 +- firestore/src/tests/validation_test.cc | 2 +- .../SilentRejectionSingleThreadExecutor.java | 33 --------------- 7 files changed, 9 insertions(+), 86 deletions(-) delete mode 100644 firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc index 71afb8cf80..226e1e7c12 100644 --- a/firestore/src/android/document_reference_android.cc +++ b/firestore/src/android/document_reference_android.cc @@ -40,8 +40,7 @@ namespace firestore { "[Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;"), \ X(Delete, "delete", "()Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotListener, "addSnapshotListener", \ - "(Ljava/util/concurrent/Executor;" \ - "Lcom/google/firebase/firestore/MetadataChanges;" \ + "(Lcom/google/firebase/firestore/MetadataChanges;" \ "Lcom/google/firebase/firestore/EventListener;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on @@ -247,7 +246,7 @@ ListenerRegistration DocumentReferenceInternal::AddSnapshotListener( jobject java_registration = env->CallObjectMethod( obj_, document_reference::GetMethodId(document_reference::kAddSnapshotListener), - firestore_->user_callback_executor(), java_metadata, java_listener); + java_metadata, java_listener); env->DeleteLocalRef(java_listener); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index e0509f6bc2..2fbafec34a 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -81,7 +81,7 @@ const char kApiIdentifier[] = "Firestore"; X(ClearPersistence, "clearPersistence", \ "()Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotsInSyncListener, "addSnapshotsInSyncListener", \ - "(Ljava/util/concurrent/Executor;Ljava/lang/Runnable;)" \ + "(Ljava/lang/Runnable;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on @@ -92,15 +92,6 @@ METHOD_LOOKUP_DEFINITION(firebase_firestore, "com/google/firebase/firestore/FirebaseFirestore", FIREBASE_FIRESTORE_METHODS) -#define SILENT_REJECTION_EXECUTOR_METHODS(X) X(Constructor, "", "()V") -METHOD_LOOKUP_DECLARATION(silent_rejection_executor, - SILENT_REJECTION_EXECUTOR_METHODS) -METHOD_LOOKUP_DEFINITION(silent_rejection_executor, - PROGUARD_KEEP_CLASS - "com/google/firebase/firestore/internal/cpp/" - "SilentRejectionSingleThreadExecutor", - SILENT_REJECTION_EXECUTOR_METHODS) - Mutex FirestoreInternal::init_mutex_; // NOLINT int FirestoreInternal::initialize_count_ = 0; @@ -127,16 +118,6 @@ FirestoreInternal::FirestoreInternal(App* app) { // default, we may safely remove the calls below. set_settings(settings()); - jobject user_callback_executor_obj = - env->NewObject(silent_rejection_executor::GetClass(), - silent_rejection_executor::GetMethodId( - silent_rejection_executor::kConstructor)); - - CheckAndClearJniExceptions(env); - FIREBASE_ASSERT(user_callback_executor_obj != nullptr); - user_callback_executor_ = env->NewGlobalRef(user_callback_executor_obj); - env->DeleteLocalRef(user_callback_executor_obj); - future_manager_.AllocFutureApi(this, static_cast(FirestoreFn::kCount)); } @@ -198,17 +179,13 @@ bool FirestoreInternal::InitializeEmbeddedClasses(App* app) { ::firebase_firestore::firestore_resources_size)); return EventListenerInternal::InitializeEmbeddedClasses(app, &embedded_files) && - TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files) && - silent_rejection_executor::CacheClassFromFiles(env, activity, - &embedded_files) && - silent_rejection_executor::CacheMethodIds(env, activity); + TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files); } /* static */ void FirestoreInternal::ReleaseClasses(App* app) { JNIEnv* env = app->GetJNIEnv(); firebase_firestore::ReleaseClass(env); - silent_rejection_executor::ReleaseClass(env); util::CheckAndClearJniExceptions(env); // Call Terminate on each Firestore internal class. @@ -249,13 +226,6 @@ void FirestoreInternal::Terminate(App* app) { } } -void FirestoreInternal::ShutdownUserCallbackExecutor() { - JNIEnv* env = app_->GetJNIEnv(); - auto shutdown_method = env->GetMethodID( - env->GetObjectClass(user_callback_executor_), "shutdown", "()V"); - env->CallVoidMethod(user_callback_executor_, shutdown_method); -} - FirestoreInternal::~FirestoreInternal() { // If initialization failed, there is nothing to clean up. if (app_ == nullptr) return; @@ -273,11 +243,7 @@ FirestoreInternal::~FirestoreInternal() { future_manager_.ReleaseFutureApi(this); - ShutdownUserCallbackExecutor(); - JNIEnv* env = app_->GetJNIEnv(); - env->DeleteGlobalRef(user_callback_executor_); - user_callback_executor_ = nullptr; env->DeleteGlobalRef(obj_); obj_ = nullptr; Terminate(app_); @@ -453,7 +419,6 @@ Future FirestoreInternal::EnableNetworkLastResult() { Future FirestoreInternal::Terminate() { JNIEnv* env = app_->GetJNIEnv(); - jobject task = env->CallObjectMethod( obj_, firebase_firestore::GetMethodId(firebase_firestore::kTerminate)); CheckAndClearJniExceptions(env); @@ -462,7 +427,6 @@ Future FirestoreInternal::Terminate() { promise.RegisterForTask(FirestoreFn::kTerminate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); - return promise.GetFuture(); } @@ -519,7 +483,7 @@ ListenerRegistration FirestoreInternal::AddSnapshotsInSyncListener( obj_, firebase_firestore::GetMethodId( firebase_firestore::kAddSnapshotsInSyncListener), - user_callback_executor(), java_runnable); + java_runnable); env->DeleteLocalRef(java_runnable); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h index 9c4cb6b536..e2533f39e1 100644 --- a/firestore/src/android/firestore_android.h +++ b/firestore/src/android/firestore_android.h @@ -151,8 +151,6 @@ class FirestoreInternal { Firestore* firestore_public() { return firestore_public_; } const Firestore* firestore_public() const { return firestore_public_; } - jobject user_callback_executor() const { return user_callback_executor_; } - private: // Gets the reference-counted Future implementation of this instance, which // can be used to create a Future. @@ -165,8 +163,6 @@ class FirestoreInternal { return static_cast&>(result); } - void ShutdownUserCallbackExecutor(); - static bool Initialize(App* app); static void ReleaseClasses(App* app); static void Terminate(App* app); @@ -177,8 +173,6 @@ class FirestoreInternal { static Mutex init_mutex_; static int initialize_count_; - jobject user_callback_executor_; - App* app_ = nullptr; Firestore* firestore_public_ = nullptr; // Java Firestore global ref. diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index 7e858406d9..be33c299f2 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -205,8 +205,8 @@ ListenerRegistration QueryInternal::AddSnapshotListener( // Register listener. jobject java_registration = env->CallObjectMethod( - obj_, query::GetMethodId(query::kAddSnapshotListener), - firestore_->user_callback_executor(), java_metadata, java_listener); + obj_, query::GetMethodId(query::kAddSnapshotListener), java_metadata, + java_listener); env->DeleteLocalRef(java_listener); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h index 9ce3243db0..8c9d089a90 100644 --- a/firestore/src/android/query_android.h +++ b/firestore/src/android/query_android.h @@ -87,8 +87,7 @@ enum class QueryFn { "(Lcom/google/firebase/firestore/Source;)" \ "Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotListener, "addSnapshotListener", \ - "(Ljava/util/concurrent/Executor;" \ - "Lcom/google/firebase/firestore/MetadataChanges;" \ + "(Lcom/google/firebase/firestore/MetadataChanges;" \ "Lcom/google/firebase/firestore/EventListener;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on diff --git a/firestore/src/tests/validation_test.cc b/firestore/src/tests/validation_test.cc index 61be692f37..2cb243a194 100644 --- a/firestore/src/tests/validation_test.cc +++ b/firestore/src/tests/validation_test.cc @@ -670,7 +670,7 @@ TEST_F(ValidationTest, Await(firestore()->EnableNetwork()); Await(future); - snapshot = accumulator.AwaitRemoteEvent(); + snapshot = accumulator.Await(); EXPECT_FALSE(snapshot.metadata().has_pending_writes()); EXPECT_NO_THROW(collection.OrderBy(FieldPath({"timestamp"})) .EndAt(snapshot.documents().at(0)) diff --git a/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java b/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java deleted file mode 100644 index e66defc7b3..0000000000 --- a/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.google.firebase.firestore.internal.cpp; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; - -/** - * Simple {@code Executor} implementation wraps around a single threaded executor and swallows - * {@code RejectedExecutionException} when executing commands. - * - *

    During shutdown, the C++ API must be able to prevent user callbacks from running after the - * Firestore object has been disposed. To do so, it shuts down its executors, accepting that new - * callbacks may be rejected. This class catches and discards the {@code RejectedExecutionException} - * that is thrown by the underlying Java executor after shutdown, bridging the gap between C++ - * expectations and the Java implementation. - */ -public final class SilentRejectionSingleThreadExecutor implements Executor { - private final ExecutorService internalExecutor = Executors.newSingleThreadExecutor(); - - @Override - public void execute(Runnable command) { - try { - internalExecutor.execute(command); - } catch (RejectedExecutionException e) { - // Swallow RejectedExecutionException - } - } - - public void shutdown() { - internalExecutor.shutdown(); - } -} From 51f027c359197d2d64acc53d35025da83ac188f9 Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 22 Jun 2020 20:40:46 -0700 Subject: [PATCH 012/109] Automated g4 rollback of changelist 314233757. *** Reason for rollback *** Not ready for release yet *** Original change description *** [C++] Provide a default executor with settings on iOS *** PiperOrigin-RevId: 317791137 --- firestore/src/common/settings.cc | 2 -- firestore/src/common/settings_ios.mm | 10 ---------- .../src/tests/util/integration_test_util_apple.mm | 9 +++++++++ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/firestore/src/common/settings.cc b/firestore/src/common/settings.cc index 0837584ecf..367ba8f34d 100644 --- a/firestore/src/common/settings.cc +++ b/firestore/src/common/settings.cc @@ -17,9 +17,7 @@ const char kDefaultHost[] = "firestore.googleapis.com"; } -#if !defined(__APPLE__) Settings::Settings() : host_(kDefaultHost) {} -#endif void Settings::set_host(std::string host) { host_ = firebase::Move(host); } diff --git a/firestore/src/common/settings_ios.mm b/firestore/src/common/settings_ios.mm index 016683f1ec..e839debcff 100644 --- a/firestore/src/common/settings_ios.mm +++ b/firestore/src/common/settings_ios.mm @@ -8,19 +8,9 @@ namespace firebase { namespace firestore { -namespace { - -const char kDefaultHost[] = "firestore.googleapis.com"; - -} - using util::Executor; using util::ExecutorLibdispatch; -Settings::Settings() - : host_(kDefaultHost), - executor_(Executor::CreateSerial("com.google.firebase.firestore.callback")) {} - std::unique_ptr Settings::CreateExecutor() const { return absl::make_unique(dispatch_queue()); } diff --git a/firestore/src/tests/util/integration_test_util_apple.mm b/firestore/src/tests/util/integration_test_util_apple.mm index 5125f264c8..ece42e6b40 100644 --- a/firestore/src/tests/util/integration_test_util_apple.mm +++ b/firestore/src/tests/util/integration_test_util_apple.mm @@ -15,6 +15,15 @@ void InitializeFirestore(Firestore* instance) { Firestore::set_log_level(LogLevel::kLogLevelDebug); + + // By default, Firestore runs user callbacks on the main thread; because the + // test also runs on the main thread, the callback will never be invoked. Use + // a different dispatch queue instead. + auto settings = instance->settings(); + auto queue = dispatch_queue_create("user_executor", DISPATCH_QUEUE_SERIAL); + settings.set_dispatch_queue(queue); + + instance->set_settings(settings); } } // namespace firestore From a46c63a9345145326f8538955946aa5f6561833f Mon Sep 17 00:00:00 2001 From: mcg Date: Fri, 26 Jun 2020 13:55:50 -0700 Subject: [PATCH 013/109] Fix a missing call to `ClearListeners` in ~FirestoreInternal. Also fix a memory leak during listener unregistration PiperOrigin-RevId: 318538053 --- firestore/src/ios/firestore_ios.cc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/firestore/src/ios/firestore_ios.cc b/firestore/src/ios/firestore_ios.cc index dc9e20b61d..fe18cfe02b 100644 --- a/firestore/src/ios/firestore_ios.cc +++ b/firestore/src/ios/firestore_ios.cc @@ -60,12 +60,7 @@ FirestoreInternal::FirestoreInternal( } FirestoreInternal::~FirestoreInternal() { - { - std::lock_guard lock(listeners_mutex_); - HARD_ASSERT_IOS(listeners_.empty(), - "Expected all listeners to be unregistered by the time " - "FirestoreInternal is destroyed."); - } + ClearListeners(); firestore_core_->Dispose(); } @@ -248,6 +243,7 @@ void FirestoreInternal::ClearListeners() { std::lock_guard lock(listeners_mutex_); for (auto* listener : listeners_) { listener->Remove(); + delete listener; } listeners_.clear(); } From cef677b0a0aa4d0df59a34d7a8f47e31d5da6886 Mon Sep 17 00:00:00 2001 From: mcg Date: Sat, 27 Jun 2020 13:28:45 -0700 Subject: [PATCH 014/109] Enable full stack traces for crashes in unit tests Fix leaks in firestore_test that the HeapChecker found. Test with `blaze test -c dbg` to get line numbers in stack frames. PiperOrigin-RevId: 318644593 --- firestore/src/tests/firestore_test.cc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/firestore/src/tests/firestore_test.cc b/firestore/src/tests/firestore_test.cc index 8a14257761..4547072f24 100644 --- a/firestore/src/tests/firestore_test.cc +++ b/firestore/src/tests/firestore_test.cc @@ -12,6 +12,7 @@ #include "firestore/src/android/util_android.h" #endif // defined(__ANDROID__) +#include "auth/src/include/firebase/auth.h" #include "testing/base/public/gmock.h" #include "gtest/gtest.h" @@ -25,6 +26,8 @@ namespace firebase { namespace firestore { +using ::firebase::auth::Auth; + TEST_F(FirestoreIntegrationTest, GetInstance) { // Create App. App* app = this->app(); @@ -36,6 +39,20 @@ TEST_F(FirestoreIntegrationTest, GetInstance) { EXPECT_EQ(kInitResultSuccess, result); EXPECT_NE(nullptr, instance); EXPECT_EQ(app, instance->app()); + + Auth* auth = Auth::GetAuth(app); + + // Tests normally create instances outside of those managed by + // Firestore::GetInstance. This means that in this case `instance` is a new + // one unmanaged by the test framework. If both the implicit instance and this + // instance were started they would try to use the same underlying database + // and would fail. + delete instance; + + // Firestore calls Auth::GetAuth, which implicitly creates an auth instance. + // Even though app is cleaned up automatically, the Auth instance is not. + // TODO(mcg): Figure out why App's CleanupNotifier doesn't handle Auth. + delete auth; } // Sanity test for stubs. From f62c57a4e34b47de36d06541d919688eea30b5a1 Mon Sep 17 00:00:00 2001 From: amablue Date: Mon, 29 Jun 2020 13:44:12 -0700 Subject: [PATCH 015/109] Hooked up Persistence into Realtime Database. PiperOrigin-RevId: 318885157 --- database/src/desktop/core/repo.cc | 103 +++++++++++---- database/src/desktop/core/repo.h | 5 +- database/src/desktop/database_desktop.cc | 39 ++++-- database/src/desktop/database_desktop.h | 22 ++-- .../level_db_persistence_storage_engine.cc | 119 ++++++++++++++---- .../level_db_persistence_storage_engine.h | 2 +- database/src/desktop/util_desktop_linux.cc | 22 ++-- 7 files changed, 239 insertions(+), 73 deletions(-) diff --git a/database/src/desktop/core/repo.cc b/database/src/desktop/core/repo.cc index 3a827da351..06329d6be6 100644 --- a/database/src/desktop/core/repo.cc +++ b/database/src/desktop/core/repo.cc @@ -30,7 +30,9 @@ #include "database/src/desktop/database_desktop.h" #include "database/src/desktop/database_reference_desktop.h" #include "database/src/desktop/mutable_data_desktop.h" -#include "database/src/desktop/persistence/in_memory_persistence_storage_engine.h" +#include "database/src/desktop/persistence/level_db_persistence_storage_engine.h" +#include "database/src/desktop/persistence/noop_persistence_manager.h" +#include "database/src/desktop/persistence/persistence_manager_interface.h" #include "database/src/desktop/query_desktop.h" #include "database/src/desktop/transaction_data.h" #include "database/src/desktop/util_desktop.h" @@ -77,9 +79,10 @@ class TransactionResponse : public connection::Response { }; Repo::Repo(App* app, DatabaseInternal* database, const char* url, - Logger* logger) + Logger* logger, bool persistence_enabled) : database_(database), host_info_(), + persistence_enabled_(persistence_enabled), connection_(), server_time_offset_(0), next_write_id_(0), @@ -416,49 +419,103 @@ void Repo::AckWriteAndRerunTransactions(WriteId write_id, const Path& path, AckStatus ack_status = success ? kAckConfirm : kAckRevert; std::vector events = server_sync_tree_->AckUserWrite( write_id, ack_status, kPersist, server_time_offset_); - if (events.size() > 0) { + if (!events.empty()) { RerunTransactions(path); } PostEvents(events); } -static UniquePtr InitializeSyncTree( - UniquePtr listen_provider, Logger* logger) { +static UniquePtr CreatePersistenceManager( + const char* app_data_path, LoggerBase* logger) { static const uint64_t kDefaultCacheSize = 10 * 1024 * 1024; - UniquePtr pending_write_tree = MakeUnique(); - UniquePtr persistence_storage_engine = - MakeUnique(logger); - UniquePtr tracked_query_manager = + auto persistence_storage_engine = + MakeUnique(logger); + + if (!persistence_storage_engine->Initialize(app_data_path)) { + logger->LogError("Could not initialize persistence"); + return UniquePtr(); + } + auto tracked_query_manager = MakeUnique(persistence_storage_engine.get(), logger); - UniquePtr cache_policy = - MakeUnique(kDefaultCacheSize); - UniquePtr persistence_manager = - MakeUnique(std::move(persistence_storage_engine), - std::move(tracked_query_manager), - std::move(cache_policy), logger); - return MakeUnique(std::move(pending_write_tree), - std::move(persistence_manager), - std::move(listen_provider)); + + auto cache_policy = MakeUnique(kDefaultCacheSize); + + return MakeUnique(std::move(persistence_storage_engine), + std::move(tracked_query_manager), + std::move(cache_policy), logger); } // Defers any initialization that is potentially expensive (e.g. disk access). void Repo::DeferredInitialization() { // Set up server sync tree. { - UniquePtr listen_provider = + // The path path to the on-disk persistence cache, relative to the + // per-application data directory. + std::string database_path; + + const char* package_name = database_->GetApp()->options().package_name(); + if (!package_name) { + logger_->LogError("Could not initialize persistence: No package_name."); + return; + } + + if (url_.empty()) { + logger_->LogError("Could not initialize persistence: No database url."); + return; + } + + // Skip past the https:// or http:// + auto start = url_.find("//") + strlen("//"); + std::string url_domain = url_.substr(start); + + database_path += package_name; + database_path += "/"; + database_path += url_domain; + + std::string app_data_path = GetAppDataPath(database_path.c_str()); + if (app_data_path.empty()) { + logger_->LogError( + "Could not initialize persistence: Unable to find app data " + "directory."); + return; + } + + logger_->LogDebug("app_data_path: %s", app_data_path.c_str()); + + // Set up write tree. + auto pending_write_tree = MakeUnique(); + + // Set up persistence manager + UniquePtr persistence_manager; + if (persistence_enabled_) { + persistence_manager = + CreatePersistenceManager(app_data_path.c_str(), logger_); + } else { + persistence_manager = MakeUnique(); + } + + // Set up listen provider. + auto listen_provider = MakeUnique(this, connection_.get(), logger_); WebSocketListenProvider* listen_provider_ptr = listen_provider.get(); - server_sync_tree_ = InitializeSyncTree(std::move(listen_provider), logger_); + + // Set up sync Tree. + server_sync_tree_ = MakeUnique(std::move(pending_write_tree), + std::move(persistence_manager), + std::move(listen_provider)); listen_provider_ptr->set_sync_tree(server_sync_tree_.get()); } // Set up info sync tree. { - UniquePtr listen_provider = - MakeUnique(this, &info_data_); + auto pending_write_tree = MakeUnique(); + auto persistence_manager = MakeUnique(); + auto listen_provider = MakeUnique(this, &info_data_); InfoListenProvider* listen_provider_ptr = listen_provider.get(); - info_sync_tree_ = InitializeSyncTree(std::move(listen_provider), logger_); + info_sync_tree_ = MakeUnique(std::move(pending_write_tree), + std::move(persistence_manager), + std::move(listen_provider)); listen_provider_ptr->set_sync_tree(info_sync_tree_.get()); } diff --git a/database/src/desktop/core/repo.h b/database/src/desktop/core/repo.h index cdc84235cc..32afec0247 100644 --- a/database/src/desktop/core/repo.h +++ b/database/src/desktop/core/repo.h @@ -45,7 +45,8 @@ class Repo : public connection::PersistentConnectionEventHandler { typedef firebase::internal::SafeReference ThisRef; typedef firebase::internal::SafeReferenceLock ThisRefLock; - Repo(App* app, DatabaseInternal* database, const char* url, Logger* logger); + Repo(App* app, DatabaseInternal* database, const char* url, Logger* logger, + bool persistence_enabled); ~Repo() override; @@ -172,6 +173,8 @@ class Repo : public connection::PersistentConnectionEventHandler { // The database URL. A cached version of host_info_.ToString(). std::string url_; + bool persistence_enabled_; + // Firebase websocket connection with wire protocol support UniquePtr connection_; diff --git a/database/src/desktop/database_desktop.cc b/database/src/desktop/database_desktop.cc index 316e2b85c6..e6290b0937 100644 --- a/database/src/desktop/database_desktop.cc +++ b/database/src/desktop/database_desktop.cc @@ -14,6 +14,7 @@ #include "database/src/desktop/database_desktop.h" +#include #include #include @@ -75,9 +76,10 @@ DatabaseInternal::DatabaseInternal(App* app, const char* url) : app_(app), future_manager_(), cleanup_(), + database_url_(url), constructor_url_(url), logger_(app_common::FindAppLoggerByName(app->name())), - repo_(app, this, url, &logger_) { + repo_(nullptr) { assert(app); assert(url); @@ -108,17 +110,20 @@ DatabaseInternal::~DatabaseInternal() { App* DatabaseInternal::GetApp() { return app_; } -DatabaseReference DatabaseInternal::GetReference() const { +DatabaseReference DatabaseInternal::GetReference() { + EnsureRepo(); return DatabaseReference(new DatabaseReferenceInternal( const_cast(this), Path())); } -DatabaseReference DatabaseInternal::GetReference(const char* path) const { +DatabaseReference DatabaseInternal::GetReference(const char* path) { + EnsureRepo(); return DatabaseReference(new DatabaseReferenceInternal( const_cast(this), Path(path))); } -DatabaseReference DatabaseInternal::GetReferenceFromUrl(const char* url) const { +DatabaseReference DatabaseInternal::GetReferenceFromUrl(const char* url) { + EnsureRepo(); ParseUrl parser; auto result = parser.Parse(url); if (parser.Parse(url) != ParseUrl::kParseOk) { @@ -145,6 +150,7 @@ DatabaseReference DatabaseInternal::GetReferenceFromUrl(const char* url) const { } void DatabaseInternal::GoOffline() { + EnsureRepo(); Repo::scheduler().Schedule(NewCallback( [](Repo::ThisRef ref) { Repo::ThisRefLock lock(&ref); @@ -152,10 +158,11 @@ void DatabaseInternal::GoOffline() { lock.GetReference()->connection()->Interrupt(); } }, - repo_.this_ref())); + repo_->this_ref())); } void DatabaseInternal::GoOnline() { + EnsureRepo(); Repo::scheduler().Schedule(NewCallback( [](Repo::ThisRef ref) { Repo::ThisRefLock lock(&ref); @@ -163,10 +170,11 @@ void DatabaseInternal::GoOnline() { lock.GetReference()->connection()->Resume(); } }, - repo_.this_ref())); + repo_->this_ref())); } void DatabaseInternal::PurgeOutstandingWrites() { + EnsureRepo(); Repo::scheduler().Schedule(NewCallback( [](Repo::ThisRef ref) { Repo::ThisRefLock lock(&ref); @@ -174,7 +182,7 @@ void DatabaseInternal::PurgeOutstandingWrites() { lock.GetReference()->PurgeOutstandingWrites(); } }, - repo_.this_ref())); + repo_->this_ref())); } static std::string* g_sdk_version = nullptr; @@ -185,9 +193,12 @@ const char* DatabaseInternal::GetSdkVersion() { return g_sdk_version->c_str(); } -void DatabaseInternal::SetPersistenceEnabled(bool /*enabled*/) { - // TODO(b/67910033): Support persistence. - logger_.LogWarning("Persistence is not currently supported."); +void DatabaseInternal::SetPersistenceEnabled(bool enabled) { + MutexLock lock(repo_mutex_); + // Only set persistence if the repo has not yet been initialized. + if (!repo_) { + persistence_enabled_ = enabled; + } } void DatabaseInternal::set_log_level(LogLevel log_level) { @@ -273,6 +284,14 @@ void DatabaseInternal::UnregisterAllChildListeners( } } +void DatabaseInternal::EnsureRepo() { + MutexLock lock(repo_mutex_); + if (!repo_) { + repo_ = std::make_unique(app_, this, database_url_.c_str(), &logger_, + persistence_enabled_); + } +} + } // namespace internal } // namespace database } // namespace firebase diff --git a/database/src/desktop/database_desktop.h b/database/src/desktop/database_desktop.h index a120dfd515..84291c5efa 100644 --- a/database/src/desktop/database_desktop.h +++ b/database/src/desktop/database_desktop.h @@ -16,6 +16,7 @@ #define FIREBASE_DATABASE_CLIENT_CPP_SRC_DESKTOP_DATABASE_DESKTOP_H_ #include +#include #include #include "app/src/cleanup_notifier.h" @@ -75,11 +76,11 @@ class DatabaseInternal { App* GetApp(); - DatabaseReference GetReference() const; + DatabaseReference GetReference(); - DatabaseReference GetReference(const char* path) const; + DatabaseReference GetReference(const char* path); - DatabaseReference GetReferenceFromUrl(const char* url) const; + DatabaseReference GetReferenceFromUrl(const char* url); void GoOffline(); @@ -102,7 +103,7 @@ class DatabaseInternal { // Whether this object was successfully initialized by the constructor. bool initialized() const { return app_ != nullptr; } - const char* database_url() const { return repo_.url().c_str(); } + const char* database_url() const { return database_url_.c_str(); } CleanupNotifier& cleanup() { return cleanup_; } @@ -150,7 +151,7 @@ class DatabaseInternal { return *listener_holder == listener; }); if (iter != single_value_listeners_.end()) { - repo_.RemoveEventCallback(listener, listener->query_spec()); + repo_->RemoveEventCallback(listener, listener->query_spec()); delete *iter; single_value_listeners_.erase(iter); } @@ -165,13 +166,15 @@ class DatabaseInternal { // The url that was passed to the constructor. const std::string& constructor_url() const { return constructor_url_; } - Repo* repo() { return &repo_; } + Repo* repo() { return repo_.get(); } Mutex* listener_mutex() { return &listener_mutex_; } Logger* logger() { return &logger_; } private: + void EnsureRepo(); + App* app_; ListenerCollection value_listeners_by_query_; @@ -192,15 +195,20 @@ class DatabaseInternal { // Needed to generate names that are guarenteed to be unique. PushChildNameGenerator name_generator_; + std::string database_url_; + // The url passed to the constructor (or "" if none was passed). // We keep it so that we can find the database in our cache. std::string constructor_url_; + bool persistence_enabled_; + // The logger for this instance of the database. Logger logger_; + Mutex repo_mutex_; // The local copy of the repository, for offline support and local caching. - Repo repo_; + std::unique_ptr repo_; }; } // namespace internal diff --git a/database/src/desktop/persistence/level_db_persistence_storage_engine.cc b/database/src/desktop/persistence/level_db_persistence_storage_engine.cc index 4202d8c570..b9be2674cd 100644 --- a/database/src/desktop/persistence/level_db_persistence_storage_engine.cc +++ b/database/src/desktop/persistence/level_db_persistence_storage_engine.cc @@ -11,6 +11,7 @@ // 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 "database/src/desktop/persistence/level_db_persistence_storage_engine.h" #include @@ -18,6 +19,9 @@ #include #include +// +#include + #include "app/src/assert.h" #include "app/src/include/firebase/variant.h" #include "app/src/log.h" @@ -38,17 +42,6 @@ #include "leveldb/db.h" #include "leveldb/write_batch.h" -// Special database paths -// -// These are special database paths contain data we need to track, but that -// don't want the developer or user editing. These keys are intentionally -// invalid database paths to ensure that. -static const char kDbKeyUserWriteRecords[] = "$user_write_records/"; -static const char kDbKeyTrackedQueries[] = "$tracked_queries/"; -static const char kDbKeyTrackedQueryKeys[] = "$tracked_query_keys/"; - -static const char kSeparator = '/'; - using firebase::database::internal::persistence::GetPersistedTrackedQuery; using firebase::database::internal::persistence::GetPersistedUserWriteRecord; using firebase::database::internal::persistence::PersistedTrackedQuery; @@ -64,6 +57,19 @@ using leveldb::Status; using leveldb::WriteBatch; using leveldb::WriteOptions; +// Special database paths +// +// These are special database paths contain data we need to track, but that +// don't want the developer or user editing. These keys are intentionally +// invalid database paths to ensure that. +static const char kDbKeyUserWriteRecords[] = "$user_write_records/"; +static const char kDbKeyTrackedQueries[] = "$tracked_queries/"; +static const char kDbKeyTrackedQueryKeys[] = "$tracked_query_keys/"; + +static const char kSeparator = '/'; + +static const Slice kValueSlice(".value/"); + namespace firebase { namespace database { namespace internal { @@ -176,6 +182,11 @@ bool CallOnEachLeaf(const Path& path, const Variant& variant, return true; } +static bool SliceEndsWith(const Slice& slice, const Slice& end) { + return slice.size() >= end.size() && + Slice(slice.data() + slice.size() - end.size(), end.size()) == end; +} + class BufferedWriteBatch { public: explicit BufferedWriteBatch(DB* database) @@ -200,6 +211,15 @@ class BufferedWriteBatch { } key_slice.size = buffer_.size() - key_slice.offset; + // If the key ends in .value, we prune it off to make reconstructing the + // cache simpler. This ensures that values are always stored in leveldb at + // foo/bar and never at foo/bar/.value. Since there can only be one + // representation of a value's path instead of two, rebuilding the cache is + // simpler. + if (SliceEndsWith(ToSlice(key_slice), kValueSlice)) { + key_slice.size -= kValueSlice.size(); + } + // Write value bytes to buffer. OffsetSlice& value_slice = key_value_pair.second; value_slice.offset = buffer_.size(); @@ -225,16 +245,10 @@ class BufferedWriteBatch { FIREBASE_ASSERT(error_detected_ == false); for (const KeyValuePair& key_value_pair : offset_slices_) { - const OffsetSlice& key_slice = key_value_pair.first; - const char* key_ptr = - reinterpret_cast(buffer_.data()) + key_slice.offset; - - const OffsetSlice& value_slice = key_value_pair.second; - const char* value_ptr = - reinterpret_cast(buffer_.data()) + value_slice.offset; + const OffsetSlice& key = key_value_pair.first; + const OffsetSlice& value = key_value_pair.second; - batch_.Put(Slice(key_ptr, key_slice.size), - Slice(value_ptr, value_slice.size)); + batch_.Put(ToSlice(key), ToSlice(value)); has_operation_to_write_ = true; } @@ -253,6 +267,12 @@ class BufferedWriteBatch { size_t size; }; + Slice ToSlice(const OffsetSlice& offset_slice) { + return Slice( + reinterpret_cast(buffer_.data()) + offset_slice.offset, + offset_slice.size); + } + // A key/value pair to insert into the database, represented as OffsetSlices. typedef std::pair KeyValuePair; @@ -280,11 +300,17 @@ LevelDbPersistenceStorageEngine::LevelDbPersistenceStorageEngine( : database_(nullptr), inside_transaction_(false), logger_(logger) {} bool LevelDbPersistenceStorageEngine::Initialize( - const std::string& database_path) { + const std::string& level_db_path) { Options options; options.create_if_missing = true; DB* database; - Status status = DB::Open(options, database_path, &database); + Status status = DB::Open(options, level_db_path, &database); + if (!status.ok()) { + logger_->LogError( + "Failed to initialize persistence storage engine at path %s: %s", + level_db_path.c_str(), status.ToString().c_str()); + assert(false); + } database_.reset(database); return status.ok(); } @@ -366,7 +392,6 @@ std::vector LevelDbPersistenceStorageEngine::LoadUserWrites() { } return result; } - void LevelDbPersistenceStorageEngine::RemoveAllUserWrites() { VerifyInsideTransaction(); BufferedWriteBatch buffered_write_batch(database_.get()); @@ -374,6 +399,52 @@ void LevelDbPersistenceStorageEngine::RemoveAllUserWrites() { buffered_write_batch.Commit(); } +// This adds the value into the given value at the given path. There are other +// utility functions that handle this, but they have more complex logic to +// handle all possible cases of adding a value to a variant. This version of the +// function is simpler and faster, because it can rely on the fact that all +// fields being stored are leaves (as in, not maps or vectors), and it does not +// have to deal with the rules about merging .value and .priority fields, as +// that is all handled before it is written to the database. +static void VariantAddCachedValue(Variant* variant, const Path& path, + const Variant& value) { + for (const std::string& directory : path.GetDirectories()) { + // Ensure we're operating on a map. + if (!variant->is_map()) { + // Special case: If we are adding a priority, then ensure we do not blow + // away the value, which at this point will be directly in the + // variant and not in a .value field. Note that values will never be + // stored in a .value peudofield. + if (IsPriorityKey(directory)) { + *variant = std::map{ + std::make_pair(kValueKey, std::move(*variant)), + }; + } else { + // In all other cases, just add an empty map. + *variant = Variant::EmptyMap(); + } + } + + // Get the child Variant at the given path. + auto& map = variant->map(); + + // Create the new map if necessary. + auto iter = map.find(directory); + if (iter == map.end()) { + auto insertion = map.insert(std::make_pair(directory, Variant::Null())); + iter = insertion.first; + bool success = insertion.second; + assert(success); + (void)success; + } + // Prepare the next iteration. + variant = &iter->second; + } + + // Now that we have the variant we are to operate on, insert the value in. + *variant = value; +} + Variant LevelDbPersistenceStorageEngine::ServerCache(const Path& path) { Variant result; std::string full_path; @@ -390,7 +461,7 @@ Variant LevelDbPersistenceStorageEngine::ServerCache(const Path& path) { Path key_path(child.key().ToString()); Optional relative_path = Path::GetRelative(path, key_path); assert(relative_path.has_value()); - SetVariantAtPath(&result, *relative_path, variant); + VariantAddCachedValue(&result, *relative_path, variant); } return result; } diff --git a/database/src/desktop/persistence/level_db_persistence_storage_engine.h b/database/src/desktop/persistence/level_db_persistence_storage_engine.h index d39c06afa2..4794d4fb7c 100644 --- a/database/src/desktop/persistence/level_db_persistence_storage_engine.h +++ b/database/src/desktop/persistence/level_db_persistence_storage_engine.h @@ -40,7 +40,7 @@ class LevelDbPersistenceStorageEngine : public PersistenceStorageEngine { // Opening up the database may fail, so we have to initialize the database in // a separate step. - bool Initialize(const std::string& database_path); + bool Initialize(const std::string& level_db_path); // Write data to the local cache, overwriting the data at the given path. // Additionally, log that this write occurred so that when the database is diff --git a/database/src/desktop/util_desktop_linux.cc b/database/src/desktop/util_desktop_linux.cc index d592c7f272..9f8e650aee 100644 --- a/database/src/desktop/util_desktop_linux.cc +++ b/database/src/desktop/util_desktop_linux.cc @@ -59,14 +59,22 @@ std::string GetAppDataPath(const char* app_name, bool should_create) { // these directories. if (should_create) { int retval; - retval = mkdir((home_directory + "/.local").c_str(), 0700); - if (retval != 0 && errno != EEXIST) return ""; - retval = mkdir((home_directory + "/.local/share").c_str(), 0700); - if (retval != 0 && errno != EEXIST) return ""; - retval = mkdir((home_directory + "/.local/share" + app_name).c_str(), 0700); - if (retval != 0 && errno != EEXIST) return ""; + std::string new_dirs = std::string("/.local/share/") + app_name; + + // Start is the location of the next "/" in the new_dirs string. + size_t start = 0; + while (start != std::string::npos) { + // Get the next directory. + size_t finish = new_dirs.find("/", start + 1); + home_directory.append(new_dirs, start, finish - start); + start = finish; + + // Ensure the new directory exists. + retval = mkdir(home_directory.c_str(), 0700); + if (retval != 0 && errno != EEXIST) return ""; + } } - return (home_directory + "/.local/share" + app_name); + return home_directory; } } // namespace internal From c8fd885f6204319af18b88769558748fd1b14bf4 Mon Sep 17 00:00:00 2001 From: wuandy Date: Mon, 29 Jun 2020 14:16:01 -0700 Subject: [PATCH 016/109] Automated g4 rollback of changelist 317736475. *** Reason for rollback *** re-roll cl/317154095 SKIP_FIRESTORE_KOKORO_BUILD_TEST=true *** Original change description *** Automated g4 rollback of changelist 317154095. *** Reason for rollback *** Not ready for release yet *** Original change description *** Add a user callback executor for android. Also fixes a flaky test because assertion is some times done before expected remote event arrives. *** *** PiperOrigin-RevId: 318891491 --- .../src/android/document_reference_android.cc | 5 ++- firestore/src/android/firestore_android.cc | 42 +++++++++++++++++-- firestore/src/android/firestore_android.h | 6 +++ firestore/src/android/query_android.cc | 4 +- firestore/src/android/query_android.h | 3 +- firestore/src/tests/validation_test.cc | 2 +- .../SilentRejectionSingleThreadExecutor.java | 33 +++++++++++++++ 7 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc index 226e1e7c12..71afb8cf80 100644 --- a/firestore/src/android/document_reference_android.cc +++ b/firestore/src/android/document_reference_android.cc @@ -40,7 +40,8 @@ namespace firestore { "[Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;"), \ X(Delete, "delete", "()Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotListener, "addSnapshotListener", \ - "(Lcom/google/firebase/firestore/MetadataChanges;" \ + "(Ljava/util/concurrent/Executor;" \ + "Lcom/google/firebase/firestore/MetadataChanges;" \ "Lcom/google/firebase/firestore/EventListener;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on @@ -246,7 +247,7 @@ ListenerRegistration DocumentReferenceInternal::AddSnapshotListener( jobject java_registration = env->CallObjectMethod( obj_, document_reference::GetMethodId(document_reference::kAddSnapshotListener), - java_metadata, java_listener); + firestore_->user_callback_executor(), java_metadata, java_listener); env->DeleteLocalRef(java_listener); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index 2fbafec34a..e0509f6bc2 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -81,7 +81,7 @@ const char kApiIdentifier[] = "Firestore"; X(ClearPersistence, "clearPersistence", \ "()Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotsInSyncListener, "addSnapshotsInSyncListener", \ - "(Ljava/lang/Runnable;)" \ + "(Ljava/util/concurrent/Executor;Ljava/lang/Runnable;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on @@ -92,6 +92,15 @@ METHOD_LOOKUP_DEFINITION(firebase_firestore, "com/google/firebase/firestore/FirebaseFirestore", FIREBASE_FIRESTORE_METHODS) +#define SILENT_REJECTION_EXECUTOR_METHODS(X) X(Constructor, "", "()V") +METHOD_LOOKUP_DECLARATION(silent_rejection_executor, + SILENT_REJECTION_EXECUTOR_METHODS) +METHOD_LOOKUP_DEFINITION(silent_rejection_executor, + PROGUARD_KEEP_CLASS + "com/google/firebase/firestore/internal/cpp/" + "SilentRejectionSingleThreadExecutor", + SILENT_REJECTION_EXECUTOR_METHODS) + Mutex FirestoreInternal::init_mutex_; // NOLINT int FirestoreInternal::initialize_count_ = 0; @@ -118,6 +127,16 @@ FirestoreInternal::FirestoreInternal(App* app) { // default, we may safely remove the calls below. set_settings(settings()); + jobject user_callback_executor_obj = + env->NewObject(silent_rejection_executor::GetClass(), + silent_rejection_executor::GetMethodId( + silent_rejection_executor::kConstructor)); + + CheckAndClearJniExceptions(env); + FIREBASE_ASSERT(user_callback_executor_obj != nullptr); + user_callback_executor_ = env->NewGlobalRef(user_callback_executor_obj); + env->DeleteLocalRef(user_callback_executor_obj); + future_manager_.AllocFutureApi(this, static_cast(FirestoreFn::kCount)); } @@ -179,13 +198,17 @@ bool FirestoreInternal::InitializeEmbeddedClasses(App* app) { ::firebase_firestore::firestore_resources_size)); return EventListenerInternal::InitializeEmbeddedClasses(app, &embedded_files) && - TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files); + TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files) && + silent_rejection_executor::CacheClassFromFiles(env, activity, + &embedded_files) && + silent_rejection_executor::CacheMethodIds(env, activity); } /* static */ void FirestoreInternal::ReleaseClasses(App* app) { JNIEnv* env = app->GetJNIEnv(); firebase_firestore::ReleaseClass(env); + silent_rejection_executor::ReleaseClass(env); util::CheckAndClearJniExceptions(env); // Call Terminate on each Firestore internal class. @@ -226,6 +249,13 @@ void FirestoreInternal::Terminate(App* app) { } } +void FirestoreInternal::ShutdownUserCallbackExecutor() { + JNIEnv* env = app_->GetJNIEnv(); + auto shutdown_method = env->GetMethodID( + env->GetObjectClass(user_callback_executor_), "shutdown", "()V"); + env->CallVoidMethod(user_callback_executor_, shutdown_method); +} + FirestoreInternal::~FirestoreInternal() { // If initialization failed, there is nothing to clean up. if (app_ == nullptr) return; @@ -243,7 +273,11 @@ FirestoreInternal::~FirestoreInternal() { future_manager_.ReleaseFutureApi(this); + ShutdownUserCallbackExecutor(); + JNIEnv* env = app_->GetJNIEnv(); + env->DeleteGlobalRef(user_callback_executor_); + user_callback_executor_ = nullptr; env->DeleteGlobalRef(obj_); obj_ = nullptr; Terminate(app_); @@ -419,6 +453,7 @@ Future FirestoreInternal::EnableNetworkLastResult() { Future FirestoreInternal::Terminate() { JNIEnv* env = app_->GetJNIEnv(); + jobject task = env->CallObjectMethod( obj_, firebase_firestore::GetMethodId(firebase_firestore::kTerminate)); CheckAndClearJniExceptions(env); @@ -427,6 +462,7 @@ Future FirestoreInternal::Terminate() { promise.RegisterForTask(FirestoreFn::kTerminate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); + return promise.GetFuture(); } @@ -483,7 +519,7 @@ ListenerRegistration FirestoreInternal::AddSnapshotsInSyncListener( obj_, firebase_firestore::GetMethodId( firebase_firestore::kAddSnapshotsInSyncListener), - java_runnable); + user_callback_executor(), java_runnable); env->DeleteLocalRef(java_runnable); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h index e2533f39e1..9c4cb6b536 100644 --- a/firestore/src/android/firestore_android.h +++ b/firestore/src/android/firestore_android.h @@ -151,6 +151,8 @@ class FirestoreInternal { Firestore* firestore_public() { return firestore_public_; } const Firestore* firestore_public() const { return firestore_public_; } + jobject user_callback_executor() const { return user_callback_executor_; } + private: // Gets the reference-counted Future implementation of this instance, which // can be used to create a Future. @@ -163,6 +165,8 @@ class FirestoreInternal { return static_cast&>(result); } + void ShutdownUserCallbackExecutor(); + static bool Initialize(App* app); static void ReleaseClasses(App* app); static void Terminate(App* app); @@ -173,6 +177,8 @@ class FirestoreInternal { static Mutex init_mutex_; static int initialize_count_; + jobject user_callback_executor_; + App* app_ = nullptr; Firestore* firestore_public_ = nullptr; // Java Firestore global ref. diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index be33c299f2..7e858406d9 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -205,8 +205,8 @@ ListenerRegistration QueryInternal::AddSnapshotListener( // Register listener. jobject java_registration = env->CallObjectMethod( - obj_, query::GetMethodId(query::kAddSnapshotListener), java_metadata, - java_listener); + obj_, query::GetMethodId(query::kAddSnapshotListener), + firestore_->user_callback_executor(), java_metadata, java_listener); env->DeleteLocalRef(java_listener); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h index 8c9d089a90..9ce3243db0 100644 --- a/firestore/src/android/query_android.h +++ b/firestore/src/android/query_android.h @@ -87,7 +87,8 @@ enum class QueryFn { "(Lcom/google/firebase/firestore/Source;)" \ "Lcom/google/android/gms/tasks/Task;"), \ X(AddSnapshotListener, "addSnapshotListener", \ - "(Lcom/google/firebase/firestore/MetadataChanges;" \ + "(Ljava/util/concurrent/Executor;" \ + "Lcom/google/firebase/firestore/MetadataChanges;" \ "Lcom/google/firebase/firestore/EventListener;)" \ "Lcom/google/firebase/firestore/ListenerRegistration;") // clang-format on diff --git a/firestore/src/tests/validation_test.cc b/firestore/src/tests/validation_test.cc index 2cb243a194..61be692f37 100644 --- a/firestore/src/tests/validation_test.cc +++ b/firestore/src/tests/validation_test.cc @@ -670,7 +670,7 @@ TEST_F(ValidationTest, Await(firestore()->EnableNetwork()); Await(future); - snapshot = accumulator.Await(); + snapshot = accumulator.AwaitRemoteEvent(); EXPECT_FALSE(snapshot.metadata().has_pending_writes()); EXPECT_NO_THROW(collection.OrderBy(FieldPath({"timestamp"})) .EndAt(snapshot.documents().at(0)) diff --git a/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java b/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java new file mode 100644 index 0000000000..e66defc7b3 --- /dev/null +++ b/firestore/src_java/com/google/firebase/firestore/internal/cpp/SilentRejectionSingleThreadExecutor.java @@ -0,0 +1,33 @@ +package com.google.firebase.firestore.internal.cpp; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +/** + * Simple {@code Executor} implementation wraps around a single threaded executor and swallows + * {@code RejectedExecutionException} when executing commands. + * + *

    During shutdown, the C++ API must be able to prevent user callbacks from running after the + * Firestore object has been disposed. To do so, it shuts down its executors, accepting that new + * callbacks may be rejected. This class catches and discards the {@code RejectedExecutionException} + * that is thrown by the underlying Java executor after shutdown, bridging the gap between C++ + * expectations and the Java implementation. + */ +public final class SilentRejectionSingleThreadExecutor implements Executor { + private final ExecutorService internalExecutor = Executors.newSingleThreadExecutor(); + + @Override + public void execute(Runnable command) { + try { + internalExecutor.execute(command); + } catch (RejectedExecutionException e) { + // Swallow RejectedExecutionException + } + } + + public void shutdown() { + internalExecutor.shutdown(); + } +} From e1483fe255ce8568952bc006dd091ec7461b3571 Mon Sep 17 00:00:00 2001 From: amablue Date: Mon, 29 Jun 2020 14:56:43 -0700 Subject: [PATCH 017/109] Removed errant include. PiperOrigin-RevId: 318899934 --- .../desktop/persistence/level_db_persistence_storage_engine.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/database/src/desktop/persistence/level_db_persistence_storage_engine.cc b/database/src/desktop/persistence/level_db_persistence_storage_engine.cc index b9be2674cd..58eab50365 100644 --- a/database/src/desktop/persistence/level_db_persistence_storage_engine.cc +++ b/database/src/desktop/persistence/level_db_persistence_storage_engine.cc @@ -19,9 +19,6 @@ #include #include -// -#include - #include "app/src/assert.h" #include "app/src/include/firebase/variant.h" #include "app/src/log.h" From 7a66fc4a4d418385f078a67b003263674df316f9 Mon Sep 17 00:00:00 2001 From: wuandy Date: Mon, 29 Jun 2020 18:57:38 -0700 Subject: [PATCH 018/109] Automated g4 rollback of changelist 317791137. *** Reason for rollback *** Re-roll cl/314233757 SKIP_FIRESTORE_KOKORO_BUILD_TEST=true *** Original change description *** Automated g4 rollback of changelist 314233757. *** Reason for rollback *** Not ready for release yet *** Original change description *** [C++] Provide a default executor with settings on iOS *** *** PiperOrigin-RevId: 318938740 --- firestore/src/common/settings.cc | 2 ++ firestore/src/common/settings_ios.mm | 10 ++++++++++ .../src/tests/util/integration_test_util_apple.mm | 9 --------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/firestore/src/common/settings.cc b/firestore/src/common/settings.cc index 367ba8f34d..0837584ecf 100644 --- a/firestore/src/common/settings.cc +++ b/firestore/src/common/settings.cc @@ -17,7 +17,9 @@ const char kDefaultHost[] = "firestore.googleapis.com"; } +#if !defined(__APPLE__) Settings::Settings() : host_(kDefaultHost) {} +#endif void Settings::set_host(std::string host) { host_ = firebase::Move(host); } diff --git a/firestore/src/common/settings_ios.mm b/firestore/src/common/settings_ios.mm index e839debcff..016683f1ec 100644 --- a/firestore/src/common/settings_ios.mm +++ b/firestore/src/common/settings_ios.mm @@ -8,9 +8,19 @@ namespace firebase { namespace firestore { +namespace { + +const char kDefaultHost[] = "firestore.googleapis.com"; + +} + using util::Executor; using util::ExecutorLibdispatch; +Settings::Settings() + : host_(kDefaultHost), + executor_(Executor::CreateSerial("com.google.firebase.firestore.callback")) {} + std::unique_ptr Settings::CreateExecutor() const { return absl::make_unique(dispatch_queue()); } diff --git a/firestore/src/tests/util/integration_test_util_apple.mm b/firestore/src/tests/util/integration_test_util_apple.mm index ece42e6b40..5125f264c8 100644 --- a/firestore/src/tests/util/integration_test_util_apple.mm +++ b/firestore/src/tests/util/integration_test_util_apple.mm @@ -15,15 +15,6 @@ void InitializeFirestore(Firestore* instance) { Firestore::set_log_level(LogLevel::kLogLevelDebug); - - // By default, Firestore runs user callbacks on the main thread; because the - // test also runs on the main thread, the callback will never be invoked. Use - // a different dispatch queue instead. - auto settings = instance->settings(); - auto queue = dispatch_queue_create("user_executor", DISPATCH_QUEUE_SERIAL); - settings.set_dispatch_queue(queue); - - instance->set_settings(settings); } } // namespace firestore From 4f011387a526113be5f5c966f00706fe5bdd78ea Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 29 Jun 2020 19:45:19 -0700 Subject: [PATCH 019/109] Fix issues with event accumulation in Firestore C++ integration tests There are several related flaws fixed here: * The mutex in TestEventListener was not applied consistently, leading to data races that showed up as strangely failing tests on forge. * The result of FirestoreIntegrationTest::Await was implicitly trusted to produce some results, but this isn't the case when it times out. In this error case the caller would read uninitialized memory almost immediately after, leading to crashes before the log message about the timeout had necessarily been writen. * The result of FirestoreIntegrationTest::Await was implicitly trusted not to produce more results than requested, but this didn't always happen either. This would cause failures where a test would request `n` events, and use the last `n` that arrived, skipping extra events. Now the EventAccumulator ensures that it consumes events in sequence. PiperOrigin-RevId: 318943435 --- .../src/tests/firestore_integration_test.h | 47 ++++++++++++------- firestore/src/tests/util/event_accumulator.h | 45 +++++++++++++----- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/firestore/src/tests/firestore_integration_test.h b/firestore/src/tests/firestore_integration_test.h index e22bd38db2..7115f75935 100644 --- a/firestore/src/tests/firestore_integration_test.h +++ b/firestore/src/tests/firestore_integration_test.h @@ -46,15 +46,17 @@ class TestEventListener : public EventListener { std::cout << "TestEventListener got: "; if (error == Error::kErrorOk) { std::cout << &value - << " from_cache:" << value.metadata().is_from_cache() - << " has_pending_write:" - << value.metadata().has_pending_writes() << std::endl; + << " from_cache=" << value.metadata().is_from_cache() + << " has_pending_write=" + << value.metadata().has_pending_writes() + << " event_count=" << event_count() << std::endl; } else { - std::cout << "error:" << error << std::endl; + std::cout << "error=" << error << " event_count=" << event_count() + << std::endl; } } - event_count_++; + MutexLock lock(mutex_); if (error != Error::kErrorOk) { std::cerr << "ERROR: EventListener " << name_ << " got " << error << std::endl; @@ -62,16 +64,18 @@ class TestEventListener : public EventListener { first_error_ = error; } } - MutexLock lock(mutex_); - last_result_.push_back(value); + last_results_.push_back(value); } - int event_count() const { return event_count_; } + int event_count() const { + MutexLock lock(mutex_); + return static_cast(last_results_.size()); + } const T& last_result(int i = 0) { - FIREBASE_ASSERT(i >= 0 && i < last_result_.size()); + FIREBASE_ASSERT(i >= 0 && i < last_results_.size()); MutexLock lock(mutex_); - return last_result_[last_result_.size() - 1 - i]; + return last_results_[last_results_.size() - 1 - i]; } // Hides the STLPort-related quirk that `AddSnapshotListener` has different @@ -88,23 +92,32 @@ class TestEventListener : public EventListener { #endif } - Error first_error() { return first_error_; } + Error first_error() { + MutexLock lock(mutex_); + return first_error_; + } // Set this to true to print more details for each arrived event for debug. void set_print_debug_info(bool value) { print_debug_info_ = value; } + // Copies events from the internal buffer, starting from `start`, up to but + // not including `end`. + std::vector GetEventsInRange(int start, int end) const { + MutexLock lock(mutex_); + FIREBASE_ASSERT(start <= end); + FIREBASE_ASSERT(end <= last_results_.size()); + return std::vector(last_results_.begin() + start, + last_results_.begin() + end); + } + private: - friend class EventAccumulator; + mutable Mutex mutex_; std::string name_; - int event_count_ = 0; // We may want the last N result. So we store all in a vector in the order // they arrived. - std::vector last_result_; - // We add a mutex to protect the calls to push_back, which is not thread-safe. - // Marked as `mutable` so that const functions can still be protected. - mutable Mutex mutex_; + std::vector last_results_; // We generally only check to see if there is any error. So we only store the // first non-OK error, if any. diff --git a/firestore/src/tests/util/event_accumulator.h b/firestore/src/tests/util/event_accumulator.h index 5294603d0e..70742c1a71 100644 --- a/firestore/src/tests/util/event_accumulator.h +++ b/firestore/src/tests/util/event_accumulator.h @@ -15,21 +15,35 @@ class EventAccumulator { TestEventListener* listener() { return &listener_; } std::vector Await(int num_events) { - max_events_ += num_events; - FirestoreIntegrationTest::Await(listener_, max_events_); - EXPECT_EQ(Error::kErrorOk, listener_.first_error()); - - std::vector result; - // We cannot use listener.last_result() as it goes backward and can - // contains more than max_events_ events. So we look up manually. - for (int i = max_events_ - num_events; i < max_events_; ++i) { - result.push_back(listener_.last_result_[i]); + int old_num_events = num_events_consumed_; + int desired_events = num_events_consumed_ + num_events; + FirestoreIntegrationTest::Await(listener_, desired_events); + + if (listener_.first_error() != Error::kErrorOk || + listener_.event_count() < desired_events) { + int received = listener_.event_count() - num_events_consumed_; + ADD_FAILURE() << "Failed to await " << num_events + << " events: error=" << listener_.first_error() + << ", received " << received << " events"; + + // If there are fewer events than requested, discard them. + num_events_consumed_ += received; + return {}; } - return result; + + num_events_consumed_ = desired_events; + return listener_.GetEventsInRange(old_num_events, num_events_consumed_); } - /** Await 1 event. */ - T Await() { return Await(1)[0]; } + /** Awaits 1 event. */ + T Await() { + auto events = Await(1); + if (events.empty()) { + return {}; + } else { + return events[0]; + } + } /** Waits for a snapshot with pending writes. */ T AwaitLocalEvent() { @@ -81,7 +95,12 @@ class EventAccumulator { bool IsFromCache(T event) { return event.metadata().is_from_cache(); } TestEventListener listener_; - int max_events_ = 0; + + // Total events consumed by callers of EventAccumulator. This differs from + // listener_.event_count() because that represents the number of events + // available, whereas this represents the number actually consumed. These can + // diverge if events arrive more rapidly than the tests consume them. + int num_events_consumed_ = 0; }; } // namespace firestore From c51074d3a8c08398048a42ec59ee6233306936c7 Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 29 Jun 2020 20:43:34 -0700 Subject: [PATCH 020/109] Fix test warm-up Certain tests include a warm-up step to ensure that the backend was actually available, but only waited for any event. Unfortunately this doesn't work, because when the server is unavailable, the SDK will serve a from-cache event indicating the document doesn't exist. Change all these to wait for a from-cache: false event, guaranteeing that the server is actually available and confirming the document doesn't exist. PiperOrigin-RevId: 318948824 --- firestore/src/tests/array_transform_test.cc | 2 +- firestore/src/tests/numeric_transforms_test.cc | 2 +- firestore/src/tests/server_timestamp_test.cc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/firestore/src/tests/array_transform_test.cc b/firestore/src/tests/array_transform_test.cc index 38ce96cb35..e3d8e1dfd1 100644 --- a/firestore/src/tests/array_transform_test.cc +++ b/firestore/src/tests/array_transform_test.cc @@ -24,7 +24,7 @@ class ArrayTransformTest : public FirestoreIntegrationTest { &document_, MetadataChanges::kInclude); // Wait for initial null snapshot to avoid potential races. - DocumentSnapshot snapshot = accumulator_.Await(); + DocumentSnapshot snapshot = accumulator_.AwaitServerEvent(); EXPECT_FALSE(snapshot.exists()); } diff --git a/firestore/src/tests/numeric_transforms_test.cc b/firestore/src/tests/numeric_transforms_test.cc index 79f8609a0f..1dfd404d2b 100644 --- a/firestore/src/tests/numeric_transforms_test.cc +++ b/firestore/src/tests/numeric_transforms_test.cc @@ -19,7 +19,7 @@ class NumericTransformsTest : public FirestoreIntegrationTest { accumulator_.listener()->AttachTo(&doc_ref_, MetadataChanges::kInclude); // Wait for initial null snapshot to avoid potential races. - DocumentSnapshot initial_snapshot = accumulator_.AwaitRemoteEvent(); + DocumentSnapshot initial_snapshot = accumulator_.AwaitServerEvent(); EXPECT_FALSE(initial_snapshot.exists()); } diff --git a/firestore/src/tests/server_timestamp_test.cc b/firestore/src/tests/server_timestamp_test.cc index 1e7880feec..ea8bdf8a43 100644 --- a/firestore/src/tests/server_timestamp_test.cc +++ b/firestore/src/tests/server_timestamp_test.cc @@ -29,7 +29,7 @@ class ServerTimestampTest : public FirestoreIntegrationTest { accumulator_.listener()->AttachTo(&doc_, MetadataChanges::kInclude); // Wait for initial null snapshot to avoid potential races. - DocumentSnapshot initial_snapshot = accumulator_.Await(); + DocumentSnapshot initial_snapshot = accumulator_.AwaitServerEvent(); EXPECT_FALSE(initial_snapshot.exists()); } From fef5d47861585bb1f6541a905e10212c08849665 Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 29 Jun 2020 22:38:44 -0700 Subject: [PATCH 021/109] Implement type map for public to internal types This simplifies the specification of promises, cleanup functions, and converters, since they now only need to specify the public type. PiperOrigin-RevId: 318960642 --- .../include/firebase/internal/type_traits.h | 3 + firestore/CMakeLists.txt | 1 + .../android/collection_reference_android.cc | 2 +- .../src/android/document_reference_android.cc | 10 +- firestore/src/android/query_android.cc | 2 +- .../src/android/query_snapshot_android.cc | 6 +- firestore/src/android/wrapper.h | 8 +- firestore/src/android/wrapper_future.h | 8 +- firestore/src/android/write_batch_android.cc | 2 +- firestore/src/common/cleanup.h | 4 +- firestore/src/common/document_change.cc | 3 +- firestore/src/common/document_reference.cc | 3 +- firestore/src/common/document_snapshot.cc | 3 +- firestore/src/common/listener_registration.cc | 3 +- firestore/src/common/query.cc | 2 +- firestore/src/common/query_snapshot.cc | 2 +- firestore/src/common/transaction.cc | 2 +- firestore/src/common/type_mapping.h | 94 +++++++++++++++++++ firestore/src/common/wrapper_assertions.h | 10 +- firestore/src/common/write_batch.cc | 2 +- firestore/src/ios/converter_ios.h | 70 +++++--------- firestore/src/tests/field_value_test.cc | 4 +- firestore/src/tests/query_test.cc | 4 +- firestore/src/tests/write_batch_test.cc | 4 +- 24 files changed, 163 insertions(+), 89 deletions(-) create mode 100644 firestore/src/common/type_mapping.h diff --git a/app/src/include/firebase/internal/type_traits.h b/app/src/include/firebase/internal/type_traits.h index ca2fe90c41..6b31f77206 100644 --- a/app/src/include/firebase/internal/type_traits.h +++ b/app/src/include/firebase/internal/type_traits.h @@ -82,6 +82,9 @@ struct is_lvalue_reference { #define FIREBASE_TYPE_TRAITS_NS std #endif +template +using decay = FIREBASE_TYPE_TRAITS_NS::decay; + template using enable_if = FIREBASE_TYPE_TRAITS_NS::enable_if; diff --git a/firestore/CMakeLists.txt b/firestore/CMakeLists.txt index 4841c65082..5c081e6de7 100644 --- a/firestore/CMakeLists.txt +++ b/firestore/CMakeLists.txt @@ -38,6 +38,7 @@ set(common_SRCS src/common/snapshot_metadata.cc src/common/to_string.cc src/common/to_string.h + src/common/type_mapping.h src/common/transaction.cc src/common/util.cc src/common/util.h diff --git a/firestore/src/android/collection_reference_android.cc b/firestore/src/android/collection_reference_android.cc index 4d7a555d9c..48d0b916b7 100644 --- a/firestore/src/android/collection_reference_android.cc +++ b/firestore/src/android/collection_reference_android.cc @@ -114,7 +114,7 @@ Future CollectionReferenceInternal::Add( map_value.java_object()); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(CollectionReferenceFn::kAdd, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc index 71afb8cf80..900cfc501e 100644 --- a/firestore/src/android/document_reference_android.cc +++ b/firestore/src/android/document_reference_android.cc @@ -119,7 +119,7 @@ Future DocumentReferenceInternal::Get(Source source) { SourceInternal::ToJavaObject(env, source)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kGet, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -142,7 +142,7 @@ Future DocumentReferenceInternal::Set(const MapFieldValue& data, env->DeleteLocalRef(java_options); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kSet, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -161,7 +161,7 @@ Future DocumentReferenceInternal::Update(const MapFieldValue& data) { map_value.java_object()); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kUpdate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -190,7 +190,7 @@ Future DocumentReferenceInternal::Update(const MapFieldPathValue& data) { env->DeleteLocalRef(more_fields_and_values); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kUpdate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -207,7 +207,7 @@ Future DocumentReferenceInternal::Delete() { obj_, document_reference::GetMethodId(document_reference::kDelete)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kDelete, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index 7e858406d9..d10c40f70a 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -79,7 +79,7 @@ Future QueryInternal::Get(Source source) { SourceInternal::ToJavaObject(env, source)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(QueryFn::kGet, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); diff --git a/firestore/src/android/query_snapshot_android.cc b/firestore/src/android/query_snapshot_android.cc index ae5714da5d..36b43e72e8 100644 --- a/firestore/src/android/query_snapshot_android.cc +++ b/firestore/src/android/query_snapshot_android.cc @@ -65,8 +65,7 @@ std::vector QuerySnapshotInternal::DocumentChanges( CheckAndClearJniExceptions(env); std::vector result; - JavaListToStdVector( - firestore_, change_list, &result); + JavaListToStdVector(firestore_, change_list, &result); return result; } @@ -77,8 +76,7 @@ std::vector QuerySnapshotInternal::documents() const { CheckAndClearJniExceptions(env); std::vector result; - JavaListToStdVector( - firestore_, document_list, &result); + JavaListToStdVector(firestore_, document_list, &result); env->DeleteLocalRef(document_list); return result; } diff --git a/firestore/src/android/wrapper.h b/firestore/src/android/wrapper.h index 2b8a9f9e35..67e1d3f880 100644 --- a/firestore/src/android/wrapper.h +++ b/firestore/src/android/wrapper.h @@ -8,6 +8,7 @@ #include "app/src/util_android.h" #include "firestore/src/android/firestore_android.h" #include "firestore/src/android/util_android.h" +#include "firestore/src/common/type_mapping.h" #include "firestore/src/include/firebase/firestore/field_path.h" #include "firestore/src/include/firebase/firestore/field_value.h" #include "firestore/src/include/firebase/firestore/map_field_value.h" @@ -67,9 +68,9 @@ class Wrapper { // Converts a java list of java type e.g. java.util.List to // a C++ vector of equivalent type e.g. std::vector. - template + template > static void JavaListToStdVector(FirestoreInternal* firestore, jobject from, - std::vector* to) { + std::vector* to) { JNIEnv* env = firestore->app()->GetJNIEnv(); int size = env->CallIntMethod(from, util::list::GetMethodId(util::list::kSize)); @@ -81,8 +82,7 @@ class Wrapper { from, util::list::GetMethodId(util::list::kGet), i); CheckAndClearJniExceptions(env); // Cannot call with emplace_back since the constructor is protected. - to->push_back( - FirestoreType{new FirestoreTypeInternal{firestore, element}}); + to->push_back(PublicT{new InternalT{firestore, element}}); env->DeleteLocalRef(element); } } diff --git a/firestore/src/android/wrapper_future.h b/firestore/src/android/wrapper_future.h index 5f41e273f7..ded5115dd9 100644 --- a/firestore/src/android/wrapper_future.h +++ b/firestore/src/android/wrapper_future.h @@ -6,6 +6,7 @@ #include "app/meta/move.h" #include "firestore/src/android/promise_android.h" #include "firestore/src/android/wrapper.h" +#include "firestore/src/common/type_mapping.h" namespace firebase { namespace firestore { @@ -47,10 +48,9 @@ class WrapperFuture : public Wrapper { // Creates a Promise representing the completion of an underlying Java Task. // This can be used to implement APIs that return Futures of some public type. // Use MakePromise() to create a Future. - template - Promise MakePromise() { - return Promise{ref_future(), - firestore_}; + template > + Promise MakePromise() { + return Promise{ref_future(), firestore_}; } // A helper that generalizes the logic for FooLastResult() of each Foo() diff --git a/firestore/src/android/write_batch_android.cc b/firestore/src/android/write_batch_android.cc index 583851b658..bd6ebe2c0f 100644 --- a/firestore/src/android/write_batch_android.cc +++ b/firestore/src/android/write_batch_android.cc @@ -104,7 +104,7 @@ Future WriteBatchInternal::Commit() { obj_, write_batch::GetMethodId(write_batch::kCommit)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = MakePromise(); promise.RegisterForTask(WriteBatchFn::kCommit, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); diff --git a/firestore/src/common/cleanup.h b/firestore/src/common/cleanup.h index fbf59d78c4..14b542cd87 100644 --- a/firestore/src/common/cleanup.h +++ b/firestore/src/common/cleanup.h @@ -3,6 +3,7 @@ #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_CLEANUP_H_ #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_CLEANUP_H_ +#include "firestore/src/common/type_mapping.h" #include "firestore/src/include/firebase/firestore/listener_registration.h" namespace firebase { @@ -16,7 +17,8 @@ class FirestoreInternal; // F is almost always FirestoreInternal unless one wants something else to // manage the cleanup process. We define type F to make this CleanupFn // implementation platform-independent. -template +template , + typename F = FirestoreInternal> struct CleanupFn { static void Cleanup(void* obj_void) { DoCleanup(static_cast(obj_void)); } diff --git a/firestore/src/common/document_change.cc b/firestore/src/common/document_change.cc index fc19f1a68d..74607b4c9d 100644 --- a/firestore/src/common/document_change.cc +++ b/firestore/src/common/document_change.cc @@ -17,8 +17,7 @@ namespace firebase { namespace firestore { -using CleanupFnDocumentChange = - CleanupFn; +using CleanupFnDocumentChange = CleanupFn; using Type = DocumentChange::Type; DocumentChange::DocumentChange() {} diff --git a/firestore/src/common/document_reference.cc b/firestore/src/common/document_reference.cc index a1ef8839e1..93e1db66fd 100644 --- a/firestore/src/common/document_reference.cc +++ b/firestore/src/common/document_reference.cc @@ -24,8 +24,7 @@ namespace firebase { namespace firestore { -using CleanupFnDocumentReference = - CleanupFn; +using CleanupFnDocumentReference = CleanupFn; DocumentReference::DocumentReference() {} diff --git a/firestore/src/common/document_snapshot.cc b/firestore/src/common/document_snapshot.cc index 21b560b25d..13c5249204 100644 --- a/firestore/src/common/document_snapshot.cc +++ b/firestore/src/common/document_snapshot.cc @@ -22,8 +22,7 @@ namespace firebase { namespace firestore { -using CleanupFnDocumentSnapshot = - CleanupFn; +using CleanupFnDocumentSnapshot = CleanupFn; DocumentSnapshot::DocumentSnapshot() {} diff --git a/firestore/src/common/listener_registration.cc b/firestore/src/common/listener_registration.cc index 6b91940f5e..9d28f8f1ab 100644 --- a/firestore/src/common/listener_registration.cc +++ b/firestore/src/common/listener_registration.cc @@ -21,8 +21,7 @@ namespace firestore { // ListenerRegistrationInternal objects instead. So FirestoreInternal can // remove all listeners upon destruction. -using CleanupFnListenerRegistration = - CleanupFn; +using CleanupFnListenerRegistration = CleanupFn; ListenerRegistration::ListenerRegistration() : ListenerRegistration(nullptr) {} diff --git a/firestore/src/common/query.cc b/firestore/src/common/query.cc index a30f67f366..4dbaa175d1 100644 --- a/firestore/src/common/query.cc +++ b/firestore/src/common/query.cc @@ -23,7 +23,7 @@ namespace firebase { namespace firestore { -using CleanupFnQuery = CleanupFn; +using CleanupFnQuery = CleanupFn; Query::Query() {} diff --git a/firestore/src/common/query_snapshot.cc b/firestore/src/common/query_snapshot.cc index 23bd1f823f..7039ae50b3 100644 --- a/firestore/src/common/query_snapshot.cc +++ b/firestore/src/common/query_snapshot.cc @@ -19,7 +19,7 @@ namespace firebase { namespace firestore { -using CleanupFnQuerySnapshot = CleanupFn; +using CleanupFnQuerySnapshot = CleanupFn; QuerySnapshot::QuerySnapshot() {} diff --git a/firestore/src/common/transaction.cc b/firestore/src/common/transaction.cc index 65f55c24c2..e89e643546 100644 --- a/firestore/src/common/transaction.cc +++ b/firestore/src/common/transaction.cc @@ -16,7 +16,7 @@ namespace firebase { namespace firestore { -using CleanupFnTransaction = CleanupFn; +using CleanupFnTransaction = CleanupFn; Transaction::Transaction(TransactionInternal* internal) : internal_(internal) { FIREBASE_ASSERT(internal != nullptr); diff --git a/firestore/src/common/type_mapping.h b/firestore/src/common/type_mapping.h new file mode 100644 index 0000000000..9203c6e8e5 --- /dev/null +++ b/firestore/src/common/type_mapping.h @@ -0,0 +1,94 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_TYPE_MAPPING_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_TYPE_MAPPING_H_ + +#include "app/src/include/firebase/internal/type_traits.h" + +namespace firebase { +namespace firestore { + +class CollectionReference; +class CollectionReferenceInternal; +class DocumentChange; +class DocumentChangeInternal; +class DocumentReference; +class DocumentReferenceInternal; +class DocumentSnapshot; +class DocumentSnapshotInternal; +class FieldValue; +class FieldValueInternal; +class Firestore; +class FirestoreInternal; +class ListenerRegistration; +class ListenerRegistrationInternal; +class Query; +class QueryInternal; +class QuerySnapshot; +class QuerySnapshotInternal; +class Transaction; +class TransactionInternal; +class WriteBatch; +class WriteBatchInternal; + +// `InternalType` is the internal type corresponding to a public type T. +// For example, `InternalType` is `FirestoreInternal`. Several other +// useful mappings are included, such as `void` for `void`. + +template +struct InternalTypeMap {}; + +template <> +struct InternalTypeMap { + using type = CollectionReferenceInternal; +}; +template <> +struct InternalTypeMap { + using type = DocumentChangeInternal; +}; +template <> +struct InternalTypeMap { + using type = DocumentReferenceInternal; +}; +template <> +struct InternalTypeMap { + using type = DocumentSnapshotInternal; +}; +template <> +struct InternalTypeMap { + using type = FieldValueInternal; +}; +template <> +struct InternalTypeMap { + using type = FirestoreInternal; +}; +template <> +struct InternalTypeMap { + using type = ListenerRegistrationInternal; +}; +template <> +struct InternalTypeMap { + using type = QueryInternal; +}; +template <> +struct InternalTypeMap { + using type = QuerySnapshotInternal; +}; +template <> +struct InternalTypeMap { + using type = TransactionInternal; +}; +template <> +struct InternalTypeMap { + using type = WriteBatchInternal; +}; +template <> +struct InternalTypeMap { + using type = void; +}; + +template +using InternalType = typename InternalTypeMap::type>::type; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_TYPE_MAPPING_H_ diff --git a/firestore/src/common/wrapper_assertions.h b/firestore/src/common/wrapper_assertions.h index e2910ae73a..68aefd697a 100644 --- a/firestore/src/common/wrapper_assertions.h +++ b/firestore/src/common/wrapper_assertions.h @@ -1,7 +1,9 @@ #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_WRAPPER_ASSERTIONS_H_ #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_WRAPPER_ASSERTIONS_H_ +#include "firestore/src/common/type_mapping.h" #include "firestore/src/include/firebase/firestore.h" + #if defined(__ANDROID__) #include @@ -77,7 +79,8 @@ FirestoreTypeInternal* NewInternal() { // nullptr as its internal but we do not check that because the standard // forbids the programming practise to do anything on it after move. // Here we assume FirestoreTypeInternal is default constructable. -template +template > void AssertWrapperConstructionContract() { FirestoreType default_instance; EXPECT_EQ(nullptr, FirestoreInternal::Internal( @@ -109,8 +112,9 @@ void AssertWrapperConstructionContract() { // set to the internal_ of the other FirestoreType, which now should has // nullptr as its internal but we do not check that because the standard // forbids the programming practise to do anything on it after move. -// Here we assume irestoreTypeInternal is default constructable. -template +// Here we assume FirestoreTypeInternal is default constructable. +template > void AssertWrapperAssignmentContract() { FirestoreTypeInternal* internal = NewInternal(); FirestoreType instance = FirestoreInternal::Wrap(internal); diff --git a/firestore/src/common/write_batch.cc b/firestore/src/common/write_batch.cc index 28f38f3771..abfda2f627 100644 --- a/firestore/src/common/write_batch.cc +++ b/firestore/src/common/write_batch.cc @@ -19,7 +19,7 @@ namespace firebase { namespace firestore { -using CleanupFnWriteBatch = CleanupFn; +using CleanupFnWriteBatch = CleanupFn; WriteBatch::WriteBatch() {} diff --git a/firestore/src/ios/converter_ios.h b/firestore/src/ios/converter_ios.h index ebbcf0e75b..082a8bf486 100644 --- a/firestore/src/ios/converter_ios.h +++ b/firestore/src/ios/converter_ios.h @@ -4,6 +4,7 @@ #include #include +#include "firestore/src/common/type_mapping.h" #include "firestore/src/include/firebase/firestore.h" #include "firestore/src/ios/collection_reference_ios.h" #include "firestore/src/ios/document_change_ios.h" @@ -29,23 +30,29 @@ namespace firebase { namespace firestore { +// Additional specializations of InternalTypeMap for iOS. +template <> +struct InternalTypeMap { + using type = model::FieldPath; +}; + // The struct is just to make declaring `MakePublic` a friend easier (and // future-proof in case more parameters are added to it). struct ConverterImpl { - template + template > static PublicT MakePublicFromInternal(InternalT&& from) { auto* internal = new InternalT(std::move(from)); return PublicT{internal}; } - template + template , typename... Args> static PublicT MakePublicFromCore(CoreT&& from, Args... args) { auto* internal = new InternalT(std::move(from), std::move(args)...); return PublicT{internal}; } - template + template > static InternalT* GetInternal(PublicT* from) { return from->internal_; } @@ -54,27 +61,20 @@ struct ConverterImpl { // MakePublic inline CollectionReference MakePublic(api::CollectionReference&& from) { - return ConverterImpl::MakePublicFromCore( + return ConverterImpl::MakePublicFromCore( std::move(from)); } inline DocumentChange MakePublic(api::DocumentChange&& from) { - return ConverterImpl::MakePublicFromCore( - std::move(from)); + return ConverterImpl::MakePublicFromCore(std::move(from)); } inline DocumentReference MakePublic(api::DocumentReference&& from) { - return ConverterImpl::MakePublicFromCore( - std::move(from)); + return ConverterImpl::MakePublicFromCore(std::move(from)); } inline DocumentSnapshot MakePublic(api::DocumentSnapshot&& from) { - return ConverterImpl::MakePublicFromCore( - std::move(from)); + return ConverterImpl::MakePublicFromCore(std::move(from)); } inline FieldValue MakePublic(FieldValueInternal&& from) { @@ -84,18 +84,16 @@ inline FieldValue MakePublic(FieldValueInternal&& from) { inline ListenerRegistration MakePublic( std::unique_ptr from, FirestoreInternal* firestore) { - return ConverterImpl::MakePublicFromCore( + return ConverterImpl::MakePublicFromCore( std::move(from), firestore); } inline Query MakePublic(api::Query&& from) { - return ConverterImpl::MakePublicFromCore(from); + return ConverterImpl::MakePublicFromCore(from); } inline QuerySnapshot MakePublic(api::QuerySnapshot&& from) { - return ConverterImpl::MakePublicFromCore(from); + return ConverterImpl::MakePublicFromCore(from); } // TODO(c++17): Add a `MakePublic` overload for `Transaction`, which is not @@ -103,40 +101,18 @@ inline QuerySnapshot MakePublic(api::QuerySnapshot&& from) { // in C++17, but not in prior versions). inline WriteBatch MakePublic(api::WriteBatch&& from) { - return ConverterImpl::MakePublicFromCore( - std::move(from)); + return ConverterImpl::MakePublicFromCore(std::move(from)); } // GetInternal -inline FirestoreInternal* GetInternal(Firestore* from) { - return ConverterImpl::GetInternal(from); -} - -inline FieldValueInternal* GetInternal(FieldValue* from) { - return ConverterImpl::GetInternal(from); -} - -inline const FieldValueInternal* GetInternal(const FieldValue* from) { - return ConverterImpl::GetInternal(from); -} - -inline DocumentReferenceInternal* GetInternal(DocumentReference* from) { - return ConverterImpl::GetInternal(from); -} - -inline const DocumentSnapshotInternal* GetInternal( - const DocumentSnapshot* from) { - return ConverterImpl::GetInternal(from); -} - -inline const DocumentReferenceInternal* GetInternal( - const DocumentReference* from) { - return ConverterImpl::GetInternal(from); +template > +InternalT* GetInternal(PublicT* from) { + return ConverterImpl::GetInternal(from); } inline const model::FieldPath& GetInternal(const FieldPath& from) { - return *ConverterImpl::GetInternal(&from); + return *ConverterImpl::GetInternal(&from); } // GetCoreApi diff --git a/firestore/src/tests/field_value_test.cc b/firestore/src/tests/field_value_test.cc index aaf382ea92..7e87c255d4 100644 --- a/firestore/src/tests/field_value_test.cc +++ b/firestore/src/tests/field_value_test.cc @@ -44,11 +44,11 @@ TEST_F(FirestoreIntegrationTest, TestFieldValueTypes) { #if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) TEST_F(FieldValueTest, Construction) { - testutil::AssertWrapperConstructionContract(); + testutil::AssertWrapperConstructionContract(); } TEST_F(FieldValueTest, Assignment) { - testutil::AssertWrapperAssignmentContract(); + testutil::AssertWrapperAssignmentContract(); } #endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) diff --git a/firestore/src/tests/query_test.cc b/firestore/src/tests/query_test.cc index a9550c84b0..9855a75623 100644 --- a/firestore/src/tests/query_test.cc +++ b/firestore/src/tests/query_test.cc @@ -685,11 +685,11 @@ TEST_F(FirestoreIntegrationTest, #if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) TEST_F(QueryTest, Construction) { - testutil::AssertWrapperConstructionContract(); + testutil::AssertWrapperConstructionContract(); } TEST_F(QueryTest, Assignment) { - testutil::AssertWrapperAssignmentContract(); + testutil::AssertWrapperAssignmentContract(); } #endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) diff --git a/firestore/src/tests/write_batch_test.cc b/firestore/src/tests/write_batch_test.cc index 69c7cf8abf..28c7ecb5ce 100644 --- a/firestore/src/tests/write_batch_test.cc +++ b/firestore/src/tests/write_batch_test.cc @@ -301,11 +301,11 @@ TEST_F(WriteBatchTest, TestUpdateNestedFields) { #if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) TEST_F(WriteBatchCommonTest, Construction) { - testutil::AssertWrapperConstructionContract(); + testutil::AssertWrapperConstructionContract(); } TEST_F(WriteBatchCommonTest, Assignment) { - testutil::AssertWrapperAssignmentContract(); + testutil::AssertWrapperAssignmentContract(); } #endif // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD) From b1c369526728ae6b971e9d053f6a48eb2ff4d000 Mon Sep 17 00:00:00 2001 From: mcg Date: Tue, 30 Jun 2020 08:26:33 -0700 Subject: [PATCH 022/109] Split PromiseFactory out of WrapperFuture This makes promise creation more closely match iOS and paves the way for removing WrapperFuture altogether. Use PromiseFactory in all internal type implementations instead of extending WrapperFuture and then dDelete WrapperFuture. PiperOrigin-RevId: 319025783 --- firestore/CMakeLists.txt | 1 - .../android/collection_reference_android.cc | 4 +- .../android/collection_reference_android.h | 1 - .../src/android/document_reference_android.cc | 18 ++--- .../src/android/document_reference_android.h | 18 +++-- firestore/src/android/promise_android.h | 16 +++-- .../src/android/promise_factory_android.h | 66 ++++++++++++++++++ firestore/src/android/query_android.cc | 4 +- firestore/src/android/query_android.h | 19 ++++-- firestore/src/android/transaction_android.h | 2 +- firestore/src/android/wrapper_future.h | 68 ------------------- firestore/src/android/write_batch_android.cc | 4 +- firestore/src/android/write_batch_android.h | 18 +++-- 13 files changed, 126 insertions(+), 113 deletions(-) create mode 100644 firestore/src/android/promise_factory_android.h delete mode 100644 firestore/src/android/wrapper_future.h diff --git a/firestore/CMakeLists.txt b/firestore/CMakeLists.txt index 5c081e6de7..2f9c525c67 100644 --- a/firestore/CMakeLists.txt +++ b/firestore/CMakeLists.txt @@ -113,7 +113,6 @@ set(android_SRCS src/android/util_android.h src/android/wrapper.cc src/android/wrapper.h - src/android/wrapper_future.h src/android/write_batch_android.cc src/android/write_batch_android.h) diff --git a/firestore/src/android/collection_reference_android.cc b/firestore/src/android/collection_reference_android.cc index 48d0b916b7..48285c2a10 100644 --- a/firestore/src/android/collection_reference_android.cc +++ b/firestore/src/android/collection_reference_android.cc @@ -114,7 +114,7 @@ Future CollectionReferenceInternal::Add( map_value.java_object()); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(CollectionReferenceFn::kAdd, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -122,7 +122,7 @@ Future CollectionReferenceInternal::Add( } Future CollectionReferenceInternal::AddLastResult() { - return LastResult(CollectionReferenceFn::kAdd); + return promises_.LastResult(CollectionReferenceFn::kAdd); } /* static */ diff --git a/firestore/src/android/collection_reference_android.h b/firestore/src/android/collection_reference_android.h index 13f3dc531d..cbc6709333 100644 --- a/firestore/src/android/collection_reference_android.h +++ b/firestore/src/android/collection_reference_android.h @@ -5,7 +5,6 @@ #include "firestore/src/android/firestore_android.h" #include "firestore/src/android/query_android.h" -#include "firestore/src/android/wrapper_future.h" namespace firebase { namespace firestore { diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc index 900cfc501e..28214fa3d2 100644 --- a/firestore/src/android/document_reference_android.cc +++ b/firestore/src/android/document_reference_android.cc @@ -119,7 +119,7 @@ Future DocumentReferenceInternal::Get(Source source) { SourceInternal::ToJavaObject(env, source)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kGet, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -127,7 +127,7 @@ Future DocumentReferenceInternal::Get(Source source) { } Future DocumentReferenceInternal::GetLastResult() { - return LastResult(DocumentReferenceFn::kGet); + return promises_.LastResult(DocumentReferenceFn::kGet); } Future DocumentReferenceInternal::Set(const MapFieldValue& data, @@ -142,7 +142,7 @@ Future DocumentReferenceInternal::Set(const MapFieldValue& data, env->DeleteLocalRef(java_options); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kSet, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -150,7 +150,7 @@ Future DocumentReferenceInternal::Set(const MapFieldValue& data, } Future DocumentReferenceInternal::SetLastResult() { - return LastResult(DocumentReferenceFn::kSet); + return promises_.LastResult(DocumentReferenceFn::kSet); } Future DocumentReferenceInternal::Update(const MapFieldValue& data) { @@ -161,7 +161,7 @@ Future DocumentReferenceInternal::Update(const MapFieldValue& data) { map_value.java_object()); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kUpdate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -190,7 +190,7 @@ Future DocumentReferenceInternal::Update(const MapFieldPathValue& data) { env->DeleteLocalRef(more_fields_and_values); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kUpdate, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -198,7 +198,7 @@ Future DocumentReferenceInternal::Update(const MapFieldPathValue& data) { } Future DocumentReferenceInternal::UpdateLastResult() { - return LastResult(DocumentReferenceFn::kUpdate); + return promises_.LastResult(DocumentReferenceFn::kUpdate); } Future DocumentReferenceInternal::Delete() { @@ -207,7 +207,7 @@ Future DocumentReferenceInternal::Delete() { obj_, document_reference::GetMethodId(document_reference::kDelete)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(DocumentReferenceFn::kDelete, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -215,7 +215,7 @@ Future DocumentReferenceInternal::Delete() { } Future DocumentReferenceInternal::DeleteLastResult() { - return LastResult(DocumentReferenceFn::kDelete); + return promises_.LastResult(DocumentReferenceFn::kDelete); } #if defined(FIREBASE_USE_STD_FUNCTION) diff --git a/firestore/src/android/document_reference_android.h b/firestore/src/android/document_reference_android.h index d11bebd212..78cd1dec1a 100644 --- a/firestore/src/android/document_reference_android.h +++ b/firestore/src/android/document_reference_android.h @@ -7,7 +7,8 @@ #include "app/src/reference_counted_future_impl.h" #include "firestore/src/android/firestore_android.h" -#include "firestore/src/android/wrapper_future.h" +#include "firestore/src/android/promise_factory_android.h" +#include "firestore/src/android/wrapper.h" #include "firestore/src/include/firebase/firestore/collection_reference.h" namespace firebase { @@ -16,9 +17,9 @@ namespace firestore { class Firestore; // Each API of DocumentReference that returns a Future needs to define an enum -// value here. For example, Foo() and FooLastResult() implementation relies on -// the enum value kFoo. The enum values are used to identify and manage Future -// in the Firestore Future manager. +// value here. For example, a Future-returning method Foo() relies on the enum +// value kFoo. The enum values are used to identify and manage Future in the +// Firestore Future manager. enum class DocumentReferenceFn { kGet = 0, kSet, @@ -28,11 +29,12 @@ enum class DocumentReferenceFn { }; // This is the Android implementation of DocumentReference. -class DocumentReferenceInternal - : public WrapperFuture { +class DocumentReferenceInternal : public Wrapper { public: using ApiType = DocumentReference; - using WrapperFuture::WrapperFuture; + + DocumentReferenceInternal(FirestoreInternal* firestore, jobject object) + : Wrapper(firestore, object), promises_(firestore) {} /** Gets the Firestore instance associated with this document reference. */ Firestore* firestore(); @@ -209,6 +211,8 @@ class DocumentReferenceInternal static bool Initialize(App* app); static void Terminate(App* app); + PromiseFactory promises_; + // Below are cached call results. mutable std::string cached_id_; mutable std::string cached_path_; diff --git a/firestore/src/android/promise_android.h b/firestore/src/android/promise_android.h index bbbe1ab409..de9b149a8a 100644 --- a/firestore/src/android/promise_android.h +++ b/firestore/src/android/promise_android.h @@ -14,10 +14,13 @@ namespace firebase { namespace firestore { // This class simplifies the implementation of Future APIs for Android wrappers. -// PublicType is the public type, say Foo, and InternalType is FooInternal, -// which is required to be a subclass of WrapperFuture. FnEnumType is an enum -// class that defines a set of APIs returning a Future. For example, to -// implement Future CollectionReferenceInternal::Add(), +// PublicType is the public type, say Foo, and InternalType is FooInternal. +// FnEnumType is an enum class that defines a set of APIs returning a Future. +// +// For example, to implement: +// +// Future CollectionReferenceInternal::Add() +// // PublicType is DocumentReference, InternalType is DocumentReferenceInternal, // and FnEnumType is CollectionReferenceFn. template @@ -30,7 +33,7 @@ class Promise { template class Completion { public: - virtual ~Completion() {} + virtual ~Completion() = default; virtual void CompleteWith(Error error_code, const char* error_message, PublicT* result) = 0; }; @@ -65,7 +68,7 @@ class Promise { FirestoreInternal* firestore, Completion* completion) : impl_{impl}, firestore_{firestore}, completion_(completion) {} - virtual ~CompleterBase() {} + virtual ~CompleterBase() = default; FirestoreInternal* firestore() { return firestore_; } @@ -97,7 +100,6 @@ class Promise { error_code = Error::kErrorCancelled; break; default: - error_code = Error::kErrorUnknown; FIREBASE_ASSERT_MESSAGE(false, "unknown FutureResult %d", result_code); break; diff --git a/firestore/src/android/promise_factory_android.h b/firestore/src/android/promise_factory_android.h new file mode 100644 index 0000000000..b6f45a6d99 --- /dev/null +++ b/firestore/src/android/promise_factory_android.h @@ -0,0 +1,66 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_PROMISE_FACTORY_ANDROID_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_PROMISE_FACTORY_ANDROID_H_ + +#include "firestore/src/android/promise_android.h" +#include "firestore/src/common/type_mapping.h" + +namespace firebase { +namespace firestore { + +// Wraps a `FutureManager` and allows creating `Promise`s. `EnumT` must be an +// enumeration that lists the async API methods each of which must be backed by +// a future; it is expected to contain a member called `kCount` that stands for +// the total number of the async APIs. +template +class PromiseFactory { + public: + explicit PromiseFactory(FirestoreInternal* firestore) + : firestore_(firestore) { + firestore_->future_manager().AllocFutureApi(this, ApiCount()); + } + + PromiseFactory(const PromiseFactory& rhs) : firestore_(rhs.firestore_) { + firestore_->future_manager().AllocFutureApi(this, ApiCount()); + } + + PromiseFactory(PromiseFactory&& rhs) : firestore_(rhs.firestore_) { + firestore_->future_manager().MoveFutureApi(&rhs, this); + } + + ~PromiseFactory() { firestore_->future_manager().ReleaseFutureApi(this); } + + PromiseFactory& operator=(const PromiseFactory&) = delete; + PromiseFactory& operator=(PromiseFactory&&) = delete; + + // Creates a Promise representing the completion of an underlying Java Task. + // This can be used to implement APIs that return Futures of some public type. + // Use MakePromise() to create a Future. + template > + Promise MakePromise() { + return Promise{future_api(), firestore_}; + } + + // A helper that generalizes the logic for FooLastResult() of each Foo() + // defined. + template + Future LastResult(EnumT index) { + const auto& result = future_api()->LastResult(static_cast(index)); + return static_cast&>(result); + } + + private: + // Gets the reference-counted Future implementation of this instance, which + // can be used to create a Future. + ReferenceCountedFutureImpl* future_api() { + return firestore_->future_manager().GetFutureApi(this); + } + + constexpr int ApiCount() const { return static_cast(EnumT::kCount); } + + FirestoreInternal* firestore_ = nullptr; +}; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_PROMISE_FACTORY_ANDROID_H_ diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index d10c40f70a..8afa1ae4c5 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -79,7 +79,7 @@ Future QueryInternal::Get(Source source) { SourceInternal::ToJavaObject(env, source)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(QueryFn::kGet, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -87,7 +87,7 @@ Future QueryInternal::Get(Source source) { } Future QueryInternal::GetLastResult() { - return LastResult(QueryFn::kGet); + return promises_.LastResult(QueryFn::kGet); } /* static */ diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h index 9ce3243db0..121869b879 100644 --- a/firestore/src/android/query_android.h +++ b/firestore/src/android/query_android.h @@ -2,11 +2,13 @@ #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_QUERY_ANDROID_H_ #include + #include #include "app/src/reference_counted_future_impl.h" #include "app/src/util_android.h" -#include "firestore/src/android/wrapper_future.h" +#include "firestore/src/android/promise_factory_android.h" +#include "firestore/src/android/wrapper.h" #include "firestore/src/include/firebase/firestore/field_path.h" #include "firestore/src/include/firebase/firestore/query.h" @@ -16,9 +18,9 @@ namespace firestore { class Firestore; // Each API of Query that returns a Future needs to define an enum value here. -// For example, Foo() and FooLastResult() implementation relies on the enum -// value kFoo. The enum values are used to identify and manage Future in the -// Firestore Future manager. +// For example, a Future-returning method Foo() relies on the enum value kFoo. +// The enum values are used to identify and manage Future in the Firestore +// Future manager. enum class QueryFn { // Enum values for the baseclass Query. kGet = 0, @@ -95,10 +97,12 @@ enum class QueryFn { METHOD_LOOKUP_DECLARATION(query, QUERY_METHODS) -class QueryInternal : public WrapperFuture { +class QueryInternal : public Wrapper { public: using ApiType = Query; - using WrapperFuture::WrapperFuture; + + QueryInternal(FirestoreInternal* firestore, jobject object) + : Wrapper(firestore, object), promises_(firestore) {} /** Gets the Firestore instance associated with this query. */ Firestore* firestore(); @@ -433,6 +437,9 @@ class QueryInternal : public WrapperFuture { MetadataChanges metadata_changes, EventListener* listener, bool passing_listener_ownership = false); + protected: + PromiseFactory promises_; + private: friend class FirestoreInternal; diff --git a/firestore/src/android/transaction_android.h b/firestore/src/android/transaction_android.h index 7ea4a557f6..f1997b5592 100644 --- a/firestore/src/android/transaction_android.h +++ b/firestore/src/android/transaction_android.h @@ -6,7 +6,7 @@ #include "app/memory/shared_ptr.h" #include "app/meta/move.h" #include "app/src/embedded_file.h" -#include "firestore/src/android/wrapper_future.h" +#include "firestore/src/android/wrapper.h" #include "firestore/src/include/firebase/firestore/document_reference.h" #include "firestore/src/include/firebase/firestore/map_field_value.h" #include "firestore/src/include/firebase/firestore/transaction.h" diff --git a/firestore/src/android/wrapper_future.h b/firestore/src/android/wrapper_future.h deleted file mode 100644 index ded5115dd9..0000000000 --- a/firestore/src/android/wrapper_future.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_WRAPPER_FUTURE_H_ -#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_WRAPPER_FUTURE_H_ - -#include - -#include "app/meta/move.h" -#include "firestore/src/android/promise_android.h" -#include "firestore/src/android/wrapper.h" -#include "firestore/src/common/type_mapping.h" - -namespace firebase { -namespace firestore { - -// This is a wrapper class that has Future support. EnumType is the enum class -// type that identify Future APIs for each subclass and FnCount is the last enum -// value of EnumType to represent the number of Future APIs. -template -class WrapperFuture : public Wrapper { - public: - // A global reference will be created from obj. The caller is responsible for - // cleaning up any local references to obj after the constructor returns. - WrapperFuture(FirestoreInternal* firestore, jobject obj) - : Wrapper(firestore, obj) { - firestore_->future_manager().AllocFutureApi(this, - static_cast(FnCount)); - } - - WrapperFuture(const WrapperFuture& rhs) : Wrapper(rhs) { - firestore_->future_manager().AllocFutureApi(this, - static_cast(FnCount)); - } - - WrapperFuture(WrapperFuture&& rhs) : Wrapper(firebase::Move(rhs)) { - firestore_->future_manager().MoveFutureApi(&rhs, this); - } - - ~WrapperFuture() override { - firestore_->future_manager().ReleaseFutureApi(this); - } - - protected: - // Gets the reference-counted Future implementation of this instance, which - // can be used to create a Future. - ReferenceCountedFutureImpl* ref_future() { - return firestore_->future_manager().GetFutureApi(this); - } - - // Creates a Promise representing the completion of an underlying Java Task. - // This can be used to implement APIs that return Futures of some public type. - // Use MakePromise() to create a Future. - template > - Promise MakePromise() { - return Promise{ref_future(), firestore_}; - } - - // A helper that generalizes the logic for FooLastResult() of each Foo() - // defined. - template - Future LastResult(EnumType index) { - const auto& result = ref_future()->LastResult(static_cast(index)); - return static_cast&>(result); - } -}; - -} // namespace firestore -} // namespace firebase - -#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_WRAPPER_FUTURE_H_ diff --git a/firestore/src/android/write_batch_android.cc b/firestore/src/android/write_batch_android.cc index bd6ebe2c0f..5dcfa25dd8 100644 --- a/firestore/src/android/write_batch_android.cc +++ b/firestore/src/android/write_batch_android.cc @@ -104,7 +104,7 @@ Future WriteBatchInternal::Commit() { obj_, write_batch::GetMethodId(write_batch::kCommit)); CheckAndClearJniExceptions(env); - auto promise = MakePromise(); + auto promise = promises_.MakePromise(); promise.RegisterForTask(WriteBatchFn::kCommit, task); env->DeleteLocalRef(task); CheckAndClearJniExceptions(env); @@ -112,7 +112,7 @@ Future WriteBatchInternal::Commit() { } Future WriteBatchInternal::CommitLastResult() { - return LastResult(WriteBatchFn::kCommit); + return promises_.LastResult(WriteBatchFn::kCommit); } /* static */ diff --git a/firestore/src/android/write_batch_android.h b/firestore/src/android/write_batch_android.h index 08ba450f2b..6bfdf8cd13 100644 --- a/firestore/src/android/write_batch_android.h +++ b/firestore/src/android/write_batch_android.h @@ -1,7 +1,8 @@ #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_WRITE_BATCH_ANDROID_H_ #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_WRITE_BATCH_ANDROID_H_ -#include "firestore/src/android/wrapper_future.h" +#include "firestore/src/android/promise_factory_android.h" +#include "firestore/src/android/wrapper.h" #include "firestore/src/include/firebase/firestore/document_reference.h" #include "firestore/src/include/firebase/firestore/map_field_value.h" #include "firestore/src/include/firebase/firestore/write_batch.h" @@ -10,19 +11,20 @@ namespace firebase { namespace firestore { // Each API of WriteBatch that returns a Future needs to define an enum value -// here. For example, Foo() and FooLastResult() implementation relies on the -// enum value kFoo. The enum values are used to identify and manage Future in -// the Firestore Future manager. +// here. For example, a Future-returning method Foo() relies on the enum value +// kFoo. The enum values are used to identify and manage Future in the Firestore +// Future manager. enum class WriteBatchFn { kCommit = 0, kCount, // Must be the last enum value. }; -class WriteBatchInternal - : public WrapperFuture { +class WriteBatchInternal : public Wrapper { public: using ApiType = WriteBatch; - using WrapperFuture::WrapperFuture; + + WriteBatchInternal(FirestoreInternal* firestore, jobject object) + : Wrapper(firestore, object), promises_(firestore) {} void Set(const DocumentReference& document, const MapFieldValue& data, const SetOptions& options); @@ -42,6 +44,8 @@ class WriteBatchInternal static bool Initialize(App* app); static void Terminate(App* app); + + PromiseFactory promises_; }; } // namespace firestore From 69e733ef3fd652a9083757f74b2b9e58db154dbd Mon Sep 17 00:00:00 2001 From: mcg Date: Tue, 30 Jun 2020 09:18:15 -0700 Subject: [PATCH 023/109] Remove LastResult implementations PiperOrigin-RevId: 319034882 --- .../android/collection_reference_android.cc | 4 --- .../android/collection_reference_android.h | 9 +++-- .../src/android/document_reference_android.cc | 16 --------- .../src/android/document_reference_android.h | 33 ------------------- firestore/src/android/firestore_android.cc | 24 -------------- firestore/src/android/firestore_android.h | 17 ++-------- .../src/android/promise_factory_android.h | 8 ----- firestore/src/android/query_android.cc | 4 --- firestore/src/android/query_android.h | 9 ----- firestore/src/android/write_batch_android.cc | 4 --- firestore/src/android/write_batch_android.h | 2 -- firestore/src/ios/collection_reference_ios.cc | 5 --- firestore/src/ios/collection_reference_ios.h | 1 - firestore/src/ios/document_reference_ios.cc | 16 --------- firestore/src/ios/document_reference_ios.h | 4 --- firestore/src/ios/firestore_ios.cc | 24 -------------- firestore/src/ios/firestore_ios.h | 6 ---- firestore/src/ios/promise_factory_ios.h | 6 ---- firestore/src/ios/query_ios.cc | 4 --- firestore/src/ios/query_ios.h | 1 - firestore/src/ios/write_batch_ios.cc | 4 --- firestore/src/ios/write_batch_ios.h | 1 - .../src/stub/collection_reference_stub.h | 3 -- firestore/src/stub/document_reference_stub.h | 6 ---- firestore/src/stub/firestore_stub.h | 9 ----- firestore/src/stub/write_batch_stub.h | 2 -- 26 files changed, 7 insertions(+), 215 deletions(-) diff --git a/firestore/src/android/collection_reference_android.cc b/firestore/src/android/collection_reference_android.cc index 48285c2a10..4170892604 100644 --- a/firestore/src/android/collection_reference_android.cc +++ b/firestore/src/android/collection_reference_android.cc @@ -121,10 +121,6 @@ Future CollectionReferenceInternal::Add( return promise.GetFuture(); } -Future CollectionReferenceInternal::AddLastResult() { - return promises_.LastResult(CollectionReferenceFn::kAdd); -} - /* static */ bool CollectionReferenceInternal::Initialize(App* app) { JNIEnv* env = app->GetJNIEnv(); diff --git a/firestore/src/android/collection_reference_android.h b/firestore/src/android/collection_reference_android.h index cbc6709333..3b1073be2b 100644 --- a/firestore/src/android/collection_reference_android.h +++ b/firestore/src/android/collection_reference_android.h @@ -11,10 +11,10 @@ namespace firestore { // To make things simple, CollectionReferenceInternal uses the Future management // from its base class, QueryInternal. Each API of CollectionReference that -// returns a Future needs to define an enum value to QueryFn. For example, Foo() -// and FooLastResult() implementation relies on the enum value QueryFn::kFoo. -// The enum values are used to identify and manage Future in the Firestore -// Future manager. +// returns a Future needs to define an enum value to QueryFn. For example, a +// Future-returning method Foo() relies on the enum value QueryFn::kFoo. The +// enum values are used to identify and manage Future in the Firestore Future +// manager. using CollectionReferenceFn = QueryFn; // This is the Android implementation of CollectionReference. @@ -29,7 +29,6 @@ class CollectionReferenceInternal : public QueryInternal { DocumentReference Document() const; DocumentReference Document(const std::string& document_path) const; Future Add(const MapFieldValue& data); - Future AddLastResult(); private: friend class FirestoreInternal; diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc index 28214fa3d2..cb51943ad9 100644 --- a/firestore/src/android/document_reference_android.cc +++ b/firestore/src/android/document_reference_android.cc @@ -126,10 +126,6 @@ Future DocumentReferenceInternal::Get(Source source) { return promise.GetFuture(); } -Future DocumentReferenceInternal::GetLastResult() { - return promises_.LastResult(DocumentReferenceFn::kGet); -} - Future DocumentReferenceInternal::Set(const MapFieldValue& data, const SetOptions& options) { FieldValueInternal map_value(data); @@ -149,10 +145,6 @@ Future DocumentReferenceInternal::Set(const MapFieldValue& data, return promise.GetFuture(); } -Future DocumentReferenceInternal::SetLastResult() { - return promises_.LastResult(DocumentReferenceFn::kSet); -} - Future DocumentReferenceInternal::Update(const MapFieldValue& data) { FieldValueInternal map_value(data); JNIEnv* env = firestore_->app()->GetJNIEnv(); @@ -197,10 +189,6 @@ Future DocumentReferenceInternal::Update(const MapFieldPathValue& data) { return promise.GetFuture(); } -Future DocumentReferenceInternal::UpdateLastResult() { - return promises_.LastResult(DocumentReferenceFn::kUpdate); -} - Future DocumentReferenceInternal::Delete() { JNIEnv* env = firestore_->app()->GetJNIEnv(); jobject task = env->CallObjectMethod( @@ -214,10 +202,6 @@ Future DocumentReferenceInternal::Delete() { return promise.GetFuture(); } -Future DocumentReferenceInternal::DeleteLastResult() { - return promises_.LastResult(DocumentReferenceFn::kDelete); -} - #if defined(FIREBASE_USE_STD_FUNCTION) ListenerRegistration DocumentReferenceInternal::AddSnapshotListener( diff --git a/firestore/src/android/document_reference_android.h b/firestore/src/android/document_reference_android.h index 78cd1dec1a..5fa010e4ea 100644 --- a/firestore/src/android/document_reference_android.h +++ b/firestore/src/android/document_reference_android.h @@ -84,14 +84,6 @@ class DocumentReferenceInternal : public Wrapper { */ Future Get(Source source); - /** - * Gets the result of the most recent call to either of the Get() methods. - * - * @return The result of last call to Get() or an invalid Future, if there is - * no such call. - */ - Future GetLastResult(); - /** * Writes to this document. * @@ -105,15 +97,6 @@ class DocumentReferenceInternal : public Wrapper { */ Future Set(const MapFieldValue& data, const SetOptions& options); - /** - * Gets the result of the most recent call to either of the Set() - * methods. - * - * @return The result of last call to Set() or an invalid Future, if there is - * no such call. - */ - Future SetLastResult(); - /** * Updates fields in this document. * @@ -137,14 +120,6 @@ class DocumentReferenceInternal : public Wrapper { */ Future Update(const MapFieldPathValue& data); - /** - * Gets the result of the most recent call to Update(). - * - * @return The result of last call to Update() or an invalid Future, if there - * is no such call. - */ - Future UpdateLastResult(); - /** * Removes this document. * @@ -152,14 +127,6 @@ class DocumentReferenceInternal : public Wrapper { */ Future Delete(); - /** - * Gets the result of the most recent call to Delete(). - * - * @return The result of last call to Delete() or an invalid Future, if there - * is no such call. - */ - Future DeleteLastResult(); - #if defined(FIREBASE_USE_STD_FUNCTION) /** * @brief Starts listening to the document referenced by this diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index e0509f6bc2..a69f96f5b6 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -411,10 +411,6 @@ Future FirestoreInternal::RunTransaction( } #endif // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN) -Future FirestoreInternal::RunTransactionLastResult() { - return LastResult(FirestoreFn::kRunTransaction); -} - Future FirestoreInternal::DisableNetwork() { JNIEnv* env = app_->GetJNIEnv(); jobject task = env->CallObjectMethod( @@ -429,10 +425,6 @@ Future FirestoreInternal::DisableNetwork() { return promise.GetFuture(); } -Future FirestoreInternal::DisableNetworkLastResult() { - return LastResult(FirestoreFn::kDisableNetwork); -} - Future FirestoreInternal::EnableNetwork() { JNIEnv* env = app_->GetJNIEnv(); jobject task = env->CallObjectMethod( @@ -447,10 +439,6 @@ Future FirestoreInternal::EnableNetwork() { return promise.GetFuture(); } -Future FirestoreInternal::EnableNetworkLastResult() { - return LastResult(FirestoreFn::kEnableNetwork); -} - Future FirestoreInternal::Terminate() { JNIEnv* env = app_->GetJNIEnv(); @@ -466,10 +454,6 @@ Future FirestoreInternal::Terminate() { return promise.GetFuture(); } -Future FirestoreInternal::TerminateLastResult() { - return LastResult(FirestoreFn::kTerminate); -} - Future FirestoreInternal::WaitForPendingWrites() { JNIEnv* env = app_->GetJNIEnv(); jobject task = env->CallObjectMethod( @@ -484,10 +468,6 @@ Future FirestoreInternal::WaitForPendingWrites() { return promise.GetFuture(); } -Future FirestoreInternal::WaitForPendingWritesLastResult() { - return LastResult(FirestoreFn::kWaitForPendingWrites); -} - Future FirestoreInternal::ClearPersistence() { JNIEnv* env = app_->GetJNIEnv(); jobject task = env->CallObjectMethod( @@ -502,10 +482,6 @@ Future FirestoreInternal::ClearPersistence() { return promise.GetFuture(); } -Future FirestoreInternal::ClearPersistenceLastResult() { - return LastResult(FirestoreFn::kClearPersistence); -} - ListenerRegistration FirestoreInternal::AddSnapshotsInSyncListener( EventListener* listener, bool passing_listener_ownership) { JNIEnv* env = app_->GetJNIEnv(); diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h index 9c4cb6b536..2ab261a68a 100644 --- a/firestore/src/android/firestore_android.h +++ b/firestore/src/android/firestore_android.h @@ -33,9 +33,9 @@ class WriteBatch; extern const char kApiIdentifier[]; // Each API of Firestore that returns a Future needs to define an enum -// value here. For example, Foo() and FooLastResult() implementation relies on -// the enum value kFoo. The enum values are used to identify and manage Future -// in the Firestore Future manager. +// value here. For example, a Future-returning method Foo() relies on the enum +// value kFoo. The enum values are used to identify and manage Future in the +// Firestore Future manager. enum class FirestoreFn { kEnableNetwork = 0, kDisableNetwork, @@ -93,24 +93,18 @@ class FirestoreInternal { Future RunTransaction( std::function update); #endif // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN) - Future RunTransactionLastResult(); // Disables network and gets anything from cache instead of server. Future DisableNetwork(); - Future DisableNetworkLastResult(); // Re-enables network after a prior call to DisableNetwork(). Future EnableNetwork(); - Future EnableNetworkLastResult(); Future Terminate(); - Future TerminateLastResult(); Future WaitForPendingWrites(); - Future WaitForPendingWritesLastResult(); Future ClearPersistence(); - Future ClearPersistenceLastResult(); ListenerRegistration AddSnapshotsInSyncListener( EventListener* listener, bool passing_listener_ownership = false); @@ -160,11 +154,6 @@ class FirestoreInternal { return future_manager_.GetFutureApi(this); } - Future LastResult(FirestoreFn id) { - const auto& result = ref_future()->LastResult(static_cast(id)); - return static_cast&>(result); - } - void ShutdownUserCallbackExecutor(); static bool Initialize(App* app); diff --git a/firestore/src/android/promise_factory_android.h b/firestore/src/android/promise_factory_android.h index b6f45a6d99..438f038da1 100644 --- a/firestore/src/android/promise_factory_android.h +++ b/firestore/src/android/promise_factory_android.h @@ -40,14 +40,6 @@ class PromiseFactory { return Promise{future_api(), firestore_}; } - // A helper that generalizes the logic for FooLastResult() of each Foo() - // defined. - template - Future LastResult(EnumT index) { - const auto& result = future_api()->LastResult(static_cast(index)); - return static_cast&>(result); - } - private: // Gets the reference-counted Future implementation of this instance, which // can be used to create a Future. diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index 8afa1ae4c5..238c4086b1 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -86,10 +86,6 @@ Future QueryInternal::Get(Source source) { return promise.GetFuture(); } -Future QueryInternal::GetLastResult() { - return promises_.LastResult(QueryFn::kGet); -} - /* static */ bool QueryInternal::Initialize(App* app) { JNIEnv* env = app->GetJNIEnv(); diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h index 121869b879..8006c0c6ca 100644 --- a/firestore/src/android/query_android.h +++ b/firestore/src/android/query_android.h @@ -389,15 +389,6 @@ class QueryInternal : public Wrapper { */ virtual Future Get(Source source); - /** - * @brief Gets the result of the most recent call to either of the Get() - * methods. - * - * @return The result of last call to Get() or an invalid Future, if there is - * no such call. - */ - virtual Future GetLastResult(); - #if defined(FIREBASE_USE_STD_FUNCTION) /** * @brief Starts listening to the QuerySnapshot events referenced by this diff --git a/firestore/src/android/write_batch_android.cc b/firestore/src/android/write_batch_android.cc index 5dcfa25dd8..67edff79ee 100644 --- a/firestore/src/android/write_batch_android.cc +++ b/firestore/src/android/write_batch_android.cc @@ -111,10 +111,6 @@ Future WriteBatchInternal::Commit() { return promise.GetFuture(); } -Future WriteBatchInternal::CommitLastResult() { - return promises_.LastResult(WriteBatchFn::kCommit); -} - /* static */ bool WriteBatchInternal::Initialize(App* app) { JNIEnv* env = app->GetJNIEnv(); diff --git a/firestore/src/android/write_batch_android.h b/firestore/src/android/write_batch_android.h index 6bfdf8cd13..1d5130dbe6 100644 --- a/firestore/src/android/write_batch_android.h +++ b/firestore/src/android/write_batch_android.h @@ -37,8 +37,6 @@ class WriteBatchInternal : public Wrapper { Future Commit(); - Future CommitLastResult(); - private: friend class FirestoreInternal; diff --git a/firestore/src/ios/collection_reference_ios.cc b/firestore/src/ios/collection_reference_ios.cc index 6ec296ac9f..783be7992a 100644 --- a/firestore/src/ios/collection_reference_ios.cc +++ b/firestore/src/ios/collection_reference_ios.cc @@ -90,10 +90,5 @@ Future CollectionReferenceInternal::Add( return promise.future(); } -Future CollectionReferenceInternal::AddLastResult() { - return promise_factory().LastResult( - AsyncApis::kCollectionReferenceAdd); -} - } // namespace firestore } // namespace firebase diff --git a/firestore/src/ios/collection_reference_ios.h b/firestore/src/ios/collection_reference_ios.h index 0d34af7ff8..426449f838 100644 --- a/firestore/src/ios/collection_reference_ios.h +++ b/firestore/src/ios/collection_reference_ios.h @@ -23,7 +23,6 @@ class CollectionReferenceInternal : public QueryInternal { DocumentReference Document(const std::string& document_path) const; Future Add(const MapFieldValue& data); - Future AddLastResult(); private: const api::CollectionReference& collection_core_api() const; diff --git a/firestore/src/ios/document_reference_ios.cc b/firestore/src/ios/document_reference_ios.cc index 488e1357f1..503e75a536 100644 --- a/firestore/src/ios/document_reference_ios.cc +++ b/firestore/src/ios/document_reference_ios.cc @@ -48,10 +48,6 @@ Future DocumentReferenceInternal::Get(Source source) { return promise.future(); } -Future DocumentReferenceInternal::GetLastResult() { - return promise_factory_.LastResult(AsyncApis::kGet); -} - Future DocumentReferenceInternal::Set(const MapFieldValue& data, const SetOptions& options) { auto promise = promise_factory_.CreatePromise(AsyncApis::kSet); @@ -61,10 +57,6 @@ Future DocumentReferenceInternal::Set(const MapFieldValue& data, return promise.future(); } -Future DocumentReferenceInternal::SetLastResult() { - return promise_factory_.LastResult(AsyncApis::kSet); -} - Future DocumentReferenceInternal::Update(const MapFieldValue& data) { return UpdateImpl(user_data_converter_.ParseUpdateData(data)); } @@ -80,10 +72,6 @@ Future DocumentReferenceInternal::UpdateImpl(ParsedUpdateData&& parsed) { return promise.future(); } -Future DocumentReferenceInternal::UpdateLastResult() { - return promise_factory_.LastResult(AsyncApis::kUpdate); -} - Future DocumentReferenceInternal::Delete() { auto promise = promise_factory_.CreatePromise(AsyncApis::kDelete); auto callback = StatusCallbackWithPromise(promise); @@ -91,10 +79,6 @@ Future DocumentReferenceInternal::Delete() { return promise.future(); } -Future DocumentReferenceInternal::DeleteLastResult() { - return promise_factory_.LastResult(AsyncApis::kDelete); -} - ListenerRegistration DocumentReferenceInternal::AddSnapshotListener( MetadataChanges metadata_changes, EventListener* listener) { diff --git a/firestore/src/ios/document_reference_ios.h b/firestore/src/ios/document_reference_ios.h index 012431c0a3..911b549380 100644 --- a/firestore/src/ios/document_reference_ios.h +++ b/firestore/src/ios/document_reference_ios.h @@ -39,17 +39,13 @@ class DocumentReferenceInternal { CollectionReference Collection(const std::string& collection_path); Future Get(Source source); - Future GetLastResult(); Future Set(const MapFieldValue& data, const SetOptions& options); - Future SetLastResult(); Future Update(const MapFieldValue& data); Future Update(const MapFieldPathValue& data); - Future UpdateLastResult(); Future Delete(); - Future DeleteLastResult(); ListenerRegistration AddSnapshotListener( MetadataChanges metadata_changes, diff --git a/firestore/src/ios/firestore_ios.cc b/firestore/src/ios/firestore_ios.cc index fe18cfe02b..efe8c39288 100644 --- a/firestore/src/ios/firestore_ios.cc +++ b/firestore/src/ios/firestore_ios.cc @@ -181,10 +181,6 @@ Future FirestoreInternal::RunTransaction( return promise.future(); } -Future FirestoreInternal::RunTransactionLastResult() { - return promise_factory_.LastResult(AsyncApi::kRunTransaction); -} - Future FirestoreInternal::DisableNetwork() { auto promise = promise_factory_.CreatePromise(AsyncApi::kDisableNetwork); @@ -192,20 +188,12 @@ Future FirestoreInternal::DisableNetwork() { return promise.future(); } -Future FirestoreInternal::DisableNetworkLastResult() { - return promise_factory_.LastResult(AsyncApi::kDisableNetwork); -} - Future FirestoreInternal::EnableNetwork() { auto promise = promise_factory_.CreatePromise(AsyncApi::kEnableNetwork); firestore_core_->EnableNetwork(StatusCallbackWithPromise(promise)); return promise.future(); } -Future FirestoreInternal::EnableNetworkLastResult() { - return promise_factory_.LastResult(AsyncApi::kEnableNetwork); -} - Future FirestoreInternal::Terminate() { auto promise = promise_factory_.CreatePromise(AsyncApi::kTerminate); ClearListeners(); @@ -213,10 +201,6 @@ Future FirestoreInternal::Terminate() { return promise.future(); } -Future FirestoreInternal::TerminateLastResult() { - return promise_factory_.LastResult(AsyncApi::kTerminate); -} - Future FirestoreInternal::WaitForPendingWrites() { auto promise = promise_factory_.CreatePromise(AsyncApi::kWaitForPendingWrites); @@ -224,10 +208,6 @@ Future FirestoreInternal::WaitForPendingWrites() { return promise.future(); } -Future FirestoreInternal::WaitForPendingWritesLastResult() { - return promise_factory_.LastResult(AsyncApi::kWaitForPendingWrites); -} - Future FirestoreInternal::ClearPersistence() { auto promise = promise_factory_.CreatePromise(AsyncApi::kClearPersistence); @@ -235,10 +215,6 @@ Future FirestoreInternal::ClearPersistence() { return promise.future(); } -Future FirestoreInternal::ClearPersistenceLastResult() { - return promise_factory_.LastResult(AsyncApi::kClearPersistence); -} - void FirestoreInternal::ClearListeners() { std::lock_guard lock(listeners_mutex_); for (auto* listener : listeners_) { diff --git a/firestore/src/ios/firestore_ios.h b/firestore/src/ios/firestore_ios.h index 542bc81bc2..605a10900e 100644 --- a/firestore/src/ios/firestore_ios.h +++ b/firestore/src/ios/firestore_ios.h @@ -66,22 +66,16 @@ class FirestoreInternal { Future RunTransaction( std::function update); Future RunTransaction(TransactionFunction* update); - Future RunTransactionLastResult(); Future DisableNetwork(); - Future DisableNetworkLastResult(); Future EnableNetwork(); - Future EnableNetworkLastResult(); Future Terminate(); - Future TerminateLastResult(); Future WaitForPendingWrites(); - Future WaitForPendingWritesLastResult(); Future ClearPersistence(); - Future ClearPersistenceLastResult(); ListenerRegistration AddSnapshotsInSyncListener( EventListener* listener); diff --git a/firestore/src/ios/promise_factory_ios.h b/firestore/src/ios/promise_factory_ios.h index be463dc19e..82a38802c9 100644 --- a/firestore/src/ios/promise_factory_ios.h +++ b/firestore/src/ios/promise_factory_ios.h @@ -54,12 +54,6 @@ class PromiseFactory { return Promise{cleanup_, future_api(), static_cast(index)}; } - template - const Future& LastResult(ApiEnum index) { - const auto& result = future_api()->LastResult(static_cast(index)); - return static_cast&>(result); - } - private: ReferenceCountedFutureImpl* future_api() { return future_manager_->GetFutureApi(this); diff --git a/firestore/src/ios/query_ios.cc b/firestore/src/ios/query_ios.cc index 15477d34e4..2e1c172a1d 100644 --- a/firestore/src/ios/query_ios.cc +++ b/firestore/src/ios/query_ios.cc @@ -64,10 +64,6 @@ Future QueryInternal::Get(Source source) { return promise.future(); } -Future QueryInternal::GetLastResult() { - return promise_factory_.LastResult(AsyncApis::kGet); -} - Query QueryInternal::Where(const FieldPath& field_path, Operator op, const FieldValue& value) const { const model::FieldPath& path = GetInternal(field_path); diff --git a/firestore/src/ios/query_ios.h b/firestore/src/ios/query_ios.h index 75d9c0c3ce..fbf56fdad2 100644 --- a/firestore/src/ios/query_ios.h +++ b/firestore/src/ios/query_ios.h @@ -33,7 +33,6 @@ class QueryInternal { Query LimitToLast(int32_t limit); virtual Future Get(Source source); - virtual Future GetLastResult(); ListenerRegistration AddSnapshotListener( MetadataChanges metadata_changes, EventListener* listener); diff --git a/firestore/src/ios/write_batch_ios.cc b/firestore/src/ios/write_batch_ios.cc index cc11cdfe0f..8ff3692af3 100644 --- a/firestore/src/ios/write_batch_ios.cc +++ b/firestore/src/ios/write_batch_ios.cc @@ -58,9 +58,5 @@ Future WriteBatchInternal::Commit() { return promise.future(); } -Future WriteBatchInternal::CommitLastResult() { - return promise_factory_.LastResult(AsyncApis::kCommit); -} - } // namespace firestore } // namespace firebase diff --git a/firestore/src/ios/write_batch_ios.h b/firestore/src/ios/write_batch_ios.h index 5093ef988b..7778b607d7 100644 --- a/firestore/src/ios/write_batch_ios.h +++ b/firestore/src/ios/write_batch_ios.h @@ -28,7 +28,6 @@ class WriteBatchInternal { void Delete(const DocumentReference& document); Future Commit(); - Future CommitLastResult(); private: enum class AsyncApis { diff --git a/firestore/src/stub/collection_reference_stub.h b/firestore/src/stub/collection_reference_stub.h index 766520611b..1619254f4c 100644 --- a/firestore/src/stub/collection_reference_stub.h +++ b/firestore/src/stub/collection_reference_stub.h @@ -28,9 +28,6 @@ class CollectionReferenceInternal : public QueryInternal { Future Add(const MapFieldValue& data) { return FailedFuture(); } - Future AddLastResult() { - return FailedFuture(); - } private: std::string id_; diff --git a/firestore/src/stub/document_reference_stub.h b/firestore/src/stub/document_reference_stub.h index 3f4cd669f1..a7c0b28463 100644 --- a/firestore/src/stub/document_reference_stub.h +++ b/firestore/src/stub/document_reference_stub.h @@ -27,22 +27,16 @@ class DocumentReferenceInternal { Future Get(Source source) const { return FailedFuture(); } - Future GetLastResult() const { - return FailedFuture(); - } Future Set(const MapFieldValue& data, const SetOptions& options) { return FailedFuture(); } - Future SetLastResult() const { return FailedFuture(); } Future Update(const MapFieldValue& data) { return FailedFuture(); } Future Update(const MapFieldPathValue& data) { return FailedFuture(); } - Future UpdateLastResult() const { return FailedFuture(); } Future Delete() { return FailedFuture(); } - Future DeleteLastResult() const { return FailedFuture(); } ListenerRegistration AddSnapshotListener( MetadataChanges metadata_changes, EventListener* listener) { diff --git a/firestore/src/stub/firestore_stub.h b/firestore/src/stub/firestore_stub.h index 46d36c45fa..b8a56b37b8 100644 --- a/firestore/src/stub/firestore_stub.h +++ b/firestore/src/stub/firestore_stub.h @@ -66,26 +66,17 @@ class FirestoreInternal { } #endif // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN) - Future RunTransactionLastResult() { return FailedFuture(); } - // Disables network and gets anything from cache instead of server. Future DisableNetwork() { return FailedFuture(); } - Future DisableNetworkLastResult() { return FailedFuture(); } - // Re-enables network after a prior call to DisableNetwork(). Future EnableNetwork() { return FailedFuture(); } - Future EnableNetworkLastResult() { return FailedFuture(); } - Future Terminate() { return FailedFuture(); } - Future TerminateLastResult() { return FailedFuture(); } Future WaitForPendingWrites() { return FailedFuture(); } - Future WaitForPendingWritesLastResult() { return FailedFuture(); } Future ClearPersistence() { return FailedFuture(); } - Future ClearPersistenceLastResult() { return FailedFuture(); } ListenerRegistration AddSnapshotsInSyncListener( EventListener* listener) { diff --git a/firestore/src/stub/write_batch_stub.h b/firestore/src/stub/write_batch_stub.h index ddc4890ad2..d69a0a3496 100644 --- a/firestore/src/stub/write_batch_stub.h +++ b/firestore/src/stub/write_batch_stub.h @@ -28,8 +28,6 @@ class WriteBatchInternal { void Delete(const DocumentReference& document) {} Future Commit() { return FailedFuture(); } - - Future CommitLastResult() const { return FailedFuture(); } }; } // namespace firestore From 492dffb536cd4aacda5ad676b0ecd66e8909c51a Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 30 Jun 2020 11:04:15 -0700 Subject: [PATCH 024/109] Fix enabling debug logging from Unity in iOS. Previously, setting FirebaseFirestore.LogLevel to LogLevel.Debug would get bumped back to LogLevel.Info as an unexpected side effect of creating of a new firebase::App object. The apparent effect to users was that enabling debug logging had no effect. This made it challenging to debug issues with customers because they were unable to collect valuable debug logs when using iOS as their platform. PiperOrigin-RevId: 319056991 --- firestore/src/ios/firestore_ios.cc | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/firestore/src/ios/firestore_ios.cc b/firestore/src/ios/firestore_ios.cc index efe8c39288..647c062de6 100644 --- a/firestore/src/ios/firestore_ios.cc +++ b/firestore/src/ios/firestore_ios.cc @@ -270,21 +270,28 @@ void Firestore::set_log_level(LogLevel log_level) { case kLogLevelDebug: // Firestore doesn't have the distinction between "verbose" and "debug". LogSetLevel(util::kLogLevelDebug); - return; + break; case kLogLevelInfo: LogSetLevel(util::kLogLevelNotice); - return; + break; case kLogLevelWarning: LogSetLevel(util::kLogLevelWarning); - return; + break; case kLogLevelError: case kLogLevelAssert: // Firestore doesn't have a separate "assert" log level. LogSetLevel(util::kLogLevelError); - return; + break; + default: + UNREACHABLE(); + break; } - UNREACHABLE(); + // Call SetLogLevel() to keep the C++ log level in sync with FIRLogger's. + // Convert kLogLevelDebug to kLogLevelVerbose to force debug logs to be + // emitted. See b/159048318 for details. + firebase::SetLogLevel(log_level == kLogLevelDebug ? kLogLevelVerbose + : log_level); } } // namespace firestore From 6815e02c9f9b4d6febe93c4b650853cf7a2eda8f Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 30 Jun 2020 13:35:56 -0700 Subject: [PATCH 025/109] Cleanup listener callbacks on `DocumentReference` and `Query`. This CL is the last in a series described in cl/312713181, cl/317397413, and resolves b/156024690. In this CL the new pattern for managing listener callbacks implemented in `FirebaseFirestore.ListenForSnapshotsInSync()` is being applied to all other places where we expose listeners (i.e. `Query` and `DocumentReference`). PiperOrigin-RevId: 319089024 --- .../csharp/document_event_listener.cc | 55 +++---------------- .../firebase/csharp/document_event_listener.h | 27 ++++----- .../firebase/csharp/query_event_listener.cc | 51 +++-------------- .../firebase/csharp/query_event_listener.h | 26 ++++----- .../csharp/snapshots_in_sync_listener.cc | 5 +- 5 files changed, 39 insertions(+), 125 deletions(-) diff --git a/firestore/src/include/firebase/csharp/document_event_listener.cc b/firestore/src/include/firebase/csharp/document_event_listener.cc index 877382a8a1..46eae53789 100644 --- a/firestore/src/include/firebase/csharp/document_event_listener.cc +++ b/firestore/src/include/firebase/csharp/document_event_listener.cc @@ -7,66 +7,27 @@ namespace firebase { namespace firestore { namespace csharp { -::firebase::Mutex DocumentEventListener::g_mutex; -DocumentEventListenerCallback - DocumentEventListener::g_document_snapshot_event_listener_callback = - nullptr; - void DocumentEventListener::OnEvent(const DocumentSnapshot& value, Error error) { - MutexLock lock(g_mutex); - if (g_document_snapshot_event_listener_callback) { - firebase::callback::AddCallback(firebase::callback::NewCallback( - DocumentSnapshotEvent, callback_id_, value, error)); - } -} - -/* static */ -void DocumentEventListener::SetCallback( - DocumentEventListenerCallback callback) { - MutexLock lock(g_mutex); - if (!callback) { - g_document_snapshot_event_listener_callback = nullptr; - return; - } + // Ownership of this pointer is passed into the C# handler + auto* copy = new DocumentSnapshot(value); - if (g_document_snapshot_event_listener_callback) { - FIREBASE_ASSERT(g_document_snapshot_event_listener_callback == callback); - } else { - g_document_snapshot_event_listener_callback = callback; - } + callback_(callback_id_, copy, error); } /* static */ ListenerRegistration DocumentEventListener::AddListenerTo( - int32_t callback_id, DocumentReference reference, - MetadataChanges metadata_changes) { - DocumentEventListener listener(callback_id); - // TODO(zxu): For now, we call the one with lambda instead of EventListener - // pointer so we do not have to manage the ownership of the listener. We - // might want to extend the design to manage listeners and call the one with - // EventListener parameter e.g. adding extra parameter in the API to specify - // whether ownership is transferred. - return reference.AddSnapshotListener( + DocumentReference* reference, MetadataChanges metadata_changes, + int32_t callback_id, DocumentEventListenerCallback callback) { + DocumentEventListener listener(callback_id, callback); + + return reference->AddSnapshotListener( metadata_changes, [listener](const DocumentSnapshot& value, Error error) mutable { listener.OnEvent(value, error); }); } -/* static */ -void DocumentEventListener::DocumentSnapshotEvent(int callback_id, - DocumentSnapshot value, - Error error) { - MutexLock lock(g_mutex); - if (g_document_snapshot_event_listener_callback == nullptr) { - return; - } - // The ownership is passed through the call to C# handler. - DocumentSnapshot* copy = new DocumentSnapshot(value); - g_document_snapshot_event_listener_callback(callback_id, copy, error); -} - } // namespace csharp } // namespace firestore } // namespace firebase diff --git a/firestore/src/include/firebase/csharp/document_event_listener.h b/firestore/src/include/firebase/csharp/document_event_listener.h index 32c233fdf9..b87362f976 100644 --- a/firestore/src/include/firebase/csharp/document_event_listener.h +++ b/firestore/src/include/firebase/csharp/document_event_listener.h @@ -34,28 +34,23 @@ typedef void(SWIGSTDCALL* DocumentEventListenerCallback)(int callback_id, // can forward the calls back to the C# delegates. class DocumentEventListener : public EventListener { public: - explicit DocumentEventListener(int32_t callback_id) - : callback_id_(callback_id) {} + explicit DocumentEventListener(int32_t callback_id, + DocumentEventListenerCallback callback) + : callback_id_(callback_id), callback_(callback) {} void OnEvent(const DocumentSnapshot& value, Error error) override; - static void SetCallback(DocumentEventListenerCallback callback); - - static ListenerRegistration AddListenerTo(int32_t callback_id, - DocumentReference reference, - MetadataChanges metadataChanges); + // This method is a proxy to DocumentReference::AddSnapshotListener() + // that can be easily called from C#. It allows our C# wrapper to + // track user callbacks in a dictionary keyed off of a unique int + // for each user callback and then raise the correct one later. + static ListenerRegistration AddListenerTo( + DocumentReference* reference, MetadataChanges metadata_changes, + int32_t callback_id, DocumentEventListenerCallback callback); private: - static void DocumentSnapshotEvent(int callback_id, DocumentSnapshot value, - Error error); - int32_t callback_id_; - - // These static variables are named as global variable instead of private - // class member. - static Mutex g_mutex; - static DocumentEventListenerCallback - g_document_snapshot_event_listener_callback; + DocumentEventListenerCallback callback_; }; } // namespace csharp diff --git a/firestore/src/include/firebase/csharp/query_event_listener.cc b/firestore/src/include/firebase/csharp/query_event_listener.cc index b3dffbe598..e3207c1db8 100644 --- a/firestore/src/include/firebase/csharp/query_event_listener.cc +++ b/firestore/src/include/firebase/csharp/query_event_listener.cc @@ -7,61 +7,26 @@ namespace firebase { namespace firestore { namespace csharp { -::firebase::Mutex QueryEventListener::g_mutex; -QueryEventListenerCallback - QueryEventListener::g_query_snapshot_event_listener_callback = nullptr; - void QueryEventListener::OnEvent(const QuerySnapshot& value, Error error) { - MutexLock lock(g_mutex); - if (g_query_snapshot_event_listener_callback) { - firebase::callback::AddCallback(firebase::callback::NewCallback( - QuerySnapshotEvent, callback_id_, value, error)); - } -} - -/* static */ -void QueryEventListener::SetCallback(QueryEventListenerCallback callback) { - MutexLock lock(g_mutex); - if (!callback) { - g_query_snapshot_event_listener_callback = nullptr; - return; - } + // Ownership of this pointer is passed into the C# handler + auto* copy = new QuerySnapshot(value); - if (g_query_snapshot_event_listener_callback) { - FIREBASE_ASSERT(g_query_snapshot_event_listener_callback == callback); - } else { - g_query_snapshot_event_listener_callback = callback; - } + callback_(callback_id_, copy, error); } /* static */ ListenerRegistration QueryEventListener::AddListenerTo( - int32_t callback_id, Query query, MetadataChanges metadata_changes) { - QueryEventListener listener(callback_id); - // TODO(zxu): For now, we call the one with lambda instead of EventListener - // pointer so we do not have to manage the ownership of the listener. We - // might want to extend the design to manage listeners and call the one with - // EventListener parameter e.g. adding extra parameter in the API to specify - // whether ownership is transferred. - return query.AddSnapshotListener( + Query* query, MetadataChanges metadata_changes, int32_t callback_id, + QueryEventListenerCallback callback) { + QueryEventListener listener(callback_id, callback); + + return query->AddSnapshotListener( metadata_changes, [listener](const QuerySnapshot& value, Error error) mutable { listener.OnEvent(value, error); }); } -/* static */ -void QueryEventListener::QuerySnapshotEvent(int callback_id, - QuerySnapshot value, Error error) { - MutexLock lock(g_mutex); - if (g_query_snapshot_event_listener_callback == nullptr) { - return; - } - // The ownership is passed through the call to C# handler. - QuerySnapshot* copy = new QuerySnapshot(value); - g_query_snapshot_event_listener_callback(callback_id, copy, error); -} - } // namespace csharp } // namespace firestore } // namespace firebase diff --git a/firestore/src/include/firebase/csharp/query_event_listener.h b/firestore/src/include/firebase/csharp/query_event_listener.h index aa059b3425..38968e28b7 100644 --- a/firestore/src/include/firebase/csharp/query_event_listener.h +++ b/firestore/src/include/firebase/csharp/query_event_listener.h @@ -33,27 +33,23 @@ typedef void(SWIGSTDCALL* QueryEventListenerCallback)(int callback_id, // can forward the calls back to the C# delegates. class QueryEventListener : public EventListener { public: - QueryEventListener(int32_t callback_id) : callback_id_(callback_id) {} - - ~QueryEventListener() override {} + explicit QueryEventListener(int32_t callback_id, + QueryEventListenerCallback callback) + : callback_id_(callback_id), callback_(callback) {} void OnEvent(const QuerySnapshot& value, Error error) override; - static void SetCallback(QueryEventListenerCallback callback); - - static ListenerRegistration AddListenerTo(int32_t callback_id, Query query, - MetadataChanges metadataChanges); + // This method is a proxy to Query::AddSnapshotsListener() + // that can be easily called from C#. It allows our C# wrapper to + // track user callbacks in a dictionary keyed off of a unique int + // for each user callback and then raise the correct one later. + static ListenerRegistration AddListenerTo( + Query* query, MetadataChanges metadata_changes, int32_t callback_id, + QueryEventListenerCallback callback); private: - static void QuerySnapshotEvent(int callback_id, QuerySnapshot value, - Error error); - int32_t callback_id_; - - // These static variables are named as global variable instead of private - // class member. - static Mutex g_mutex; - static QueryEventListenerCallback g_query_snapshot_event_listener_callback; + QueryEventListenerCallback callback_; }; } // namespace csharp diff --git a/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc b/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc index 990fd2de0b..b5f3e7766a 100644 --- a/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc +++ b/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc @@ -6,10 +6,7 @@ namespace firebase { namespace firestore { namespace csharp { -void SnapshotsInSyncListener::OnEvent(Error error) { - firebase::callback::AddCallback( - firebase::callback::NewCallback(callback_, callback_id_)); -} +void SnapshotsInSyncListener::OnEvent(Error error) { callback_(callback_id_); } /* static */ ListenerRegistration SnapshotsInSyncListener::AddListenerTo( From 8714bed5e91432237032c1c47f1d3e71a493ba73 Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 1 Jul 2020 13:27:37 -0700 Subject: [PATCH 026/109] Migrate const std::string& parameters to absl::string_view. THIS CHANGE IS BELIEVED SAFE Templated asynchronous code can change the lifetime of string data as a result of this change; however, the most common uses of these (lambdas and callbacks) are excluded from this change. Further, your TAP tests pass. go/string-ref-to-string-view-lsc Tested: TAP --sample ran all affected tests and none failed http://test/OCL:319219698:BASE:319213711:1593616691507:a583f044 PiperOrigin-RevId: 319286243 --- app/rest/tests/zlibwrapper_unittest.cc | 5 +++-- app/tests/base64_openssh_test.cc | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/rest/tests/zlibwrapper_unittest.cc b/app/rest/tests/zlibwrapper_unittest.cc index d7ee8398ec..785f528a36 100644 --- a/app/rest/tests/zlibwrapper_unittest.cc +++ b/app/rest/tests/zlibwrapper_unittest.cc @@ -27,6 +27,7 @@ #include "gmock/gmock.h" #include "absl/base/macros.h" #include "absl/strings/escaping.h" +#include "absl/strings/string_view.h" #include "util/random/acmrandom.h" // 1048576 == 2^20 == 1 MB @@ -105,7 +106,7 @@ REGISTER_MODULE_INITIALIZER(zlibwrapper_unittest, { << " Reason: " << limiter.reason(); }); -bool ReadFileToString(const std::string& filename, std::string* output, +bool ReadFileToString(absl::string_view filename, std::string* output, int64 max_size) { std::ifstream f; f.open(filename); @@ -696,7 +697,7 @@ class ZLibWrapperTest : public ::testing::TestWithParam { return dict; } - std::string ReadFileToTest(const std::string& filename) { + std::string ReadFileToTest(absl::string_view filename) { std::string uncompbuf; LOG(INFO) << "Testing file: " << filename; CHECK(ReadFileToString(filename, &uncompbuf, MAX_BUF_SIZE)); diff --git a/app/tests/base64_openssh_test.cc b/app/tests/base64_openssh_test.cc index b415c6f92d..2af00dc233 100644 --- a/app/tests/base64_openssh_test.cc +++ b/app/tests/base64_openssh_test.cc @@ -18,6 +18,7 @@ #include "app/src/log.h" #include "gtest/gtest.h" #include "gmock/gmock.h" +#include "absl/strings/string_view.h" #include "openssl/base64.h" namespace firebase { @@ -31,7 +32,7 @@ size_t OpenSSHEncodedLength(size_t input_size) { return length; } -bool OpenSSHEncode(const std::string& input, std::string* output) { +bool OpenSSHEncode(absl::string_view input, std::string* output) { size_t base64_length = OpenSSHEncodedLength(input.size()); output->resize(base64_length); if (EVP_EncodeBlock(reinterpret_cast(&(*output)[0]), @@ -52,7 +53,7 @@ size_t OpenSSHDecodedLength(size_t input_size) { return length; } -bool OpenSSHDecode(const std::string& input, std::string* output) { +bool OpenSSHDecode(absl::string_view input, std::string* output) { size_t decoded_length = OpenSSHDecodedLength(input.size()); output->resize(decoded_length); if (EVP_DecodeBase64(reinterpret_cast(&(*output)[0]), From a70101ce70500bf68d0253202775c0f72499c035 Mon Sep 17 00:00:00 2001 From: mcg Date: Wed, 8 Jul 2020 14:02:01 -0700 Subject: [PATCH 027/109] Avoid double deletion of internal objects during cleanup. This has been observed during cleanup when a DocumentReferenceInternal is destroyed, its Future API can end up deleting orphaned Future APIs that contain Futures holding the containing `DocumentReference`. PiperOrigin-RevId: 320260380 --- firestore/src/common/cleanup.h | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/firestore/src/common/cleanup.h b/firestore/src/common/cleanup.h index 14b542cd87..12167c2f6a 100644 --- a/firestore/src/common/cleanup.h +++ b/firestore/src/common/cleanup.h @@ -49,8 +49,19 @@ struct CleanupFn { private: template static void DoCleanup(Object* obj) { - delete obj->internal_; + // Order is crucially important here: under rare conditions, during cleanup, + // the destructor of the `internal_` object can trigger the deletion of the + // containing object. For example, this can happen when the `internal_` + // object destroys its Future API, which deletes a Future referring to the + // public object containing this `internal_` object. See + // http://go/paste/4669581387890688 for an example of what this looks like. + // + // By setting `internal_` to null before deleting it, the destructor of the + // outer object is prevented from deleting `internal_` twice. + auto internal = obj->internal_; obj->internal_ = nullptr; + + delete internal; } // `ListenerRegistration` objects differ from the common pattern. From 7e62c360c636a4d3ed59dc382367431c685f3e2d Mon Sep 17 00:00:00 2001 From: mcg Date: Wed, 8 Jul 2020 15:01:39 -0700 Subject: [PATCH 028/109] Avoid empty statement warnings in Firebase assertions Wrapping macro bodies in do { } while(false) makes them into a statement that legitimately should be followed by a semicolon. PiperOrigin-RevId: 320272779 --- app/src/assert.h | 16 ++++++++-------- database/src/desktop/persistence/prune_forest.cc | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/assert.h b/app/src/assert.h index 0921c47f0b..7a089afa0b 100644 --- a/app/src/assert.h +++ b/app/src/assert.h @@ -45,13 +45,13 @@ // Assert condition is true, if it's false log an assert with the specified // expression as a string. #define FIREBASE_ASSERT_WITH_EXPRESSION(condition, expression) \ - { \ + do { \ if (!(condition)) { \ - FIREBASE_NAMESPACE::LogAssert( \ + FIREBASE_NAMESPACE::LogAssert( \ FIREBASE_ASSERT_MESSAGE_PREFIX FIREBASE_EXPAND_STRINGIFY( \ expression)); \ } \ - } + } while (false) // Assert condition is true, if it's false log an assert with the specified // expression as a string. Compiled out of release builds. @@ -60,7 +60,7 @@ FIREBASE_ASSERT_WITH_EXPRESSION(condition, expression) #else #define FIREBASE_DEV_ASSERT_WITH_EXPRESSION(condition, expression) \ - { (void)(condition); } + (void)(condition) #endif // !defined(NDEBUG) // Custom assert() implementation that is not compiled out in release builds. @@ -111,14 +111,14 @@ // Assert condition is true otherwise display the specified expression, // message and abort. #define FIREBASE_ASSERT_MESSAGE_WITH_EXPRESSION(condition, expression, ...) \ - { \ + do { \ if (!(condition)) { \ - FIREBASE_NAMESPACE::LogError( \ + FIREBASE_NAMESPACE::LogError( \ FIREBASE_ASSERT_MESSAGE_PREFIX FIREBASE_EXPAND_STRINGIFY( \ expression)); \ - FIREBASE_NAMESPACE::LogAssert(__VA_ARGS__); \ + FIREBASE_NAMESPACE::LogAssert(__VA_ARGS__); \ } \ - } + } while (false) // Assert condition is true otherwise display the specified expression, // message and abort. Compiled out of release builds. diff --git a/database/src/desktop/persistence/prune_forest.cc b/database/src/desktop/persistence/prune_forest.cc index 4986dc9584..217045be45 100644 --- a/database/src/desktop/persistence/prune_forest.cc +++ b/database/src/desktop/persistence/prune_forest.cc @@ -79,7 +79,7 @@ const PruneForestRef PruneForestRef::GetChild(const Path& path) const { void PruneForestRef::Prune(const Path& path) { FIREBASE_DEV_ASSERT_MESSAGE( prune_forest_->RootMostValueMatching(path, KeepPredicate) == nullptr, - "Can't prune path that was kept previously!") + "Can't prune path that was kept previously!"); if (prune_forest_->RootMostValueMatching(path, PrunePredicate) != nullptr) { // This path will already be pruned } else { From eb6696567bcaece39ace3cc94f100f1dd52effd2 Mon Sep 17 00:00:00 2001 From: mcg Date: Thu, 9 Jul 2020 12:24:44 -0700 Subject: [PATCH 029/109] Miscellaneous test-only fixes * Avoid dereferencing awaited pointers after Await has failed, preventing crashes after test timeouts. * Await `CollectionReference::Add`, preventing nondeterminism in tests. PiperOrigin-RevId: 320449430 --- .../src/tests/firestore_integration_test.cc | 47 ++++++++++++++----- .../src/tests/firestore_integration_test.h | 12 ++--- firestore/src/tests/query_network_test.cc | 4 +- firestore/src/tests/query_test.cc | 2 +- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/firestore/src/tests/firestore_integration_test.cc b/firestore/src/tests/firestore_integration_test.cc index 7462004b94..ec286f6730 100644 --- a/firestore/src/tests/firestore_integration_test.cc +++ b/firestore/src/tests/firestore_integration_test.cc @@ -141,8 +141,7 @@ void FirestoreIntegrationTest::WriteDocument(DocumentReference reference, const MapFieldValue& data) const { Future future = reference.Set(data); Await(future); - EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); - EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + FailIfUnsuccessful("WriteDocument", future); } void FirestoreIntegrationTest::WriteDocuments( @@ -157,28 +156,29 @@ DocumentSnapshot FirestoreIntegrationTest::ReadDocument( const DocumentReference& reference) const { Future future = reference.Get(); const DocumentSnapshot* result = Await(future); - EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); - EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; - EXPECT_NE(nullptr, result) << DescribeFailedFuture(future) << std::endl; - return *result; + if (FailIfUnsuccessful("ReadDocument", future)) { + return {}; + } else { + return *result; + } } QuerySnapshot FirestoreIntegrationTest::ReadDocuments( const Query& reference) const { Future future = reference.Get(); const QuerySnapshot* result = Await(future); - EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); - EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; - EXPECT_NE(nullptr, result) << DescribeFailedFuture(future) << std::endl; - return *result; + if (FailIfUnsuccessful("ReadDocuments", future)) { + return {}; + } else { + return *result; + } } void FirestoreIntegrationTest::DeleteDocument( DocumentReference reference) const { Future future = reference.Delete(); Await(future); - EXPECT_EQ(FutureStatus::kFutureStatusComplete, future.status()); - EXPECT_EQ(0, future.error()) << DescribeFailedFuture(future) << std::endl; + FailIfUnsuccessful("DeleteDocument", future); } std::vector FirestoreIntegrationTest::QuerySnapshotToIds( @@ -210,6 +210,29 @@ void FirestoreIntegrationTest::Await(const Future& future) { } } +/* static */ +bool FirestoreIntegrationTest::FailIfUnsuccessful(const char* operation, + const FutureBase& future) { + if (future.status() != FutureStatus::kFutureStatusComplete) { + ADD_FAILURE() << operation << " timed out: " << DescribeFailedFuture(future) + << std::endl; + return true; + } else if (future.error() != Error::kErrorOk) { + ADD_FAILURE() << operation << "failed: " << DescribeFailedFuture(future) + << std::endl; + return true; + } else { + return false; + } +} + +/* static */ +std::string FirestoreIntegrationTest::DescribeFailedFuture( + const FutureBase& future) { + return "WARNING: Future failed. Error code " + + std::to_string(future.error()) + ", message " + future.error_message(); +} + void FirestoreIntegrationTest::TerminateAndRelease(Firestore* firestore) { Await(firestore->Terminate()); Release(firestore); diff --git a/firestore/src/tests/firestore_integration_test.h b/firestore/src/tests/firestore_integration_test.h index 7115f75935..79bc23b1cb 100644 --- a/firestore/src/tests/firestore_integration_test.h +++ b/firestore/src/tests/firestore_integration_test.h @@ -252,12 +252,12 @@ class FirestoreIntegrationTest : public testing::Test { EXPECT_GT(cycles, 0) << "Waiting listener timed out."; } - template - static std::string DescribeFailedFuture(const Future& future) { - return "WARNING: Future failed. Error code " + - std::to_string(future.error()) + ", message " + - future.error_message(); - } + // Fails the current test if the given future did not complete or contained an + // error. Returns true if the future has failed. + static bool FailIfUnsuccessful(const char* operation, + const FutureBase& future); + + static std::string DescribeFailedFuture(const FutureBase& future); // Creates a new Firestore instance, without any caching, using a uniquely- // generated app_name. diff --git a/firestore/src/tests/query_network_test.cc b/firestore/src/tests/query_network_test.cc index f8b5627ae5..12a885c456 100644 --- a/firestore/src/tests/query_network_test.cc +++ b/firestore/src/tests/query_network_test.cc @@ -57,8 +57,10 @@ class QueryNetworkTest : public FirestoreIntegrationTest { EXPECT_TRUE(accumulator.AwaitRemoteEvent().empty()); Await(firestore()->DisableNetwork()); - collection.Add(MapFieldValue{{"foo", FieldValue::ServerTimestamp()}}); + auto added = + collection.Add(MapFieldValue{{"foo", FieldValue::ServerTimestamp()}}); Await(firestore()->EnableNetwork()); + Await(added); QuerySnapshot snapshot = accumulator.AwaitServerEvent(); EXPECT_FALSE(snapshot.empty()); diff --git a/firestore/src/tests/query_test.cc b/firestore/src/tests/query_test.cc index 9855a75623..44b67bdcee 100644 --- a/firestore/src/tests/query_test.cc +++ b/firestore/src/tests/query_test.cc @@ -438,7 +438,7 @@ TEST_F(FirestoreIntegrationTest, TestCanQueryByDocumentIdUsingRefs) { TEST_F(FirestoreIntegrationTest, TestCanQueryWithAndWithoutDocumentKey) { CollectionReference collection = Collection(); - collection.Add({}); + Await(collection.Add({})); QuerySnapshot snapshot1 = ReadDocuments(collection.OrderBy( FieldPath::DocumentId(), Query::Direction::kAscending)); QuerySnapshot snapshot2 = ReadDocuments(collection); From feb21a7e323cd45218afa7880f8ce3fc8b741226 Mon Sep 17 00:00:00 2001 From: Googler Date: Fri, 10 Jul 2020 08:40:27 -0700 Subject: [PATCH 030/109] Delete C++ Firestore objects when C# has no more references to them. This was achieved by using the CppInstanceManager, which provides this functionality elsewhere in the Unity SDK. When running UIHandlerAutomated, this eliminates all 11 leaks from Firestore::GetFirestore(). PiperOrigin-RevId: 320609048 --- .../csharp/firestore_instance_management.cc | 39 +++++++++++++++++++ .../csharp/firestore_instance_management.h | 34 ++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 firestore/src/include/firebase/csharp/firestore_instance_management.cc create mode 100644 firestore/src/include/firebase/csharp/firestore_instance_management.h diff --git a/firestore/src/include/firebase/csharp/firestore_instance_management.cc b/firestore/src/include/firebase/csharp/firestore_instance_management.cc new file mode 100644 index 0000000000..8031b8599f --- /dev/null +++ b/firestore/src/include/firebase/csharp/firestore_instance_management.cc @@ -0,0 +1,39 @@ +#include "firebase/csharp/firestore_instance_management.h" + +#include "firebase/app/client/unity/src/cpp_instance_manager.h" +#include "firebase/firestore.h" + +namespace firebase { +namespace firestore { +namespace csharp { + +namespace { + +CppInstanceManager& GetFirestoreInstanceManager() { + // Allocate the CppInstanceManager on the heap to prevent its destructor + // from executing (go/totw/110#the-fix-safe-initialization-no-destruction). + static CppInstanceManager* firestore_instance_manager = + new CppInstanceManager(); + return *firestore_instance_manager; +} + +} // namespace + +Firestore* GetFirestoreInstance(App* app) { + auto& firestore_instance_manager = GetFirestoreInstanceManager(); + // Acquire the lock used internally by CppInstanceManager::ReleaseReference() + // to avoid racing with deletion of Firestore instances. + MutexLock lock(firestore_instance_manager.mutex()); + Firestore* instance = + Firestore::GetInstance(app, /*init_result_out=*/nullptr); + firestore_instance_manager.AddReference(instance); + return instance; +} + +void ReleaseFirestoreInstance(Firestore* firestore) { + GetFirestoreInstanceManager().ReleaseReference(firestore); +} + +} // namespace csharp +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/include/firebase/csharp/firestore_instance_management.h b/firestore/src/include/firebase/csharp/firestore_instance_management.h new file mode 100644 index 0000000000..61bc89930e --- /dev/null +++ b/firestore/src/include/firebase/csharp/firestore_instance_management.h @@ -0,0 +1,34 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_FIRESTORE_INSTANCE_MANAGEMENT_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_FIRESTORE_INSTANCE_MANAGEMENT_H_ + +namespace firebase { + +class App; + +namespace firestore { + +class Firestore; + +namespace csharp { + +/** + * Returns the Firestore instance for the given App, creating it if necessary. + * This method is merely a wrapper around Firestore::GetInstance() that + * increments a reference count each time a given Firestore pointer is returned. + * The caller must call ReleaseFirestoreInstance() with the returned pointer + * once it is no longer referenced to ensure proper garbage collection. + */ +Firestore* GetFirestoreInstance(App* app); + +/** + * Decrements the reference count of the given Firestore, deleting it if the + * reference count becomes zero. The given Firestore pointer must have been + * returned by a previous invocation of GetFirestoreInstance(). + */ +void ReleaseFirestoreInstance(Firestore* firestore); + +} // namespace csharp +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_FIRESTORE_INSTANCE_MANAGEMENT_H_ From 32ee577d3700662c76c83266211cc03594d0bb97 Mon Sep 17 00:00:00 2001 From: Googler Date: Fri, 10 Jul 2020 10:24:15 -0700 Subject: [PATCH 031/109] Remove the unconditional calls to Firebase::Terminate() in test cleanup. These calls were present as a workaround for bugs in the Firestore destructor; however, since those bugs have been fixed the calls to Terminate() are now superfluous and, worse, can hide bugs. As a result, they are being removed. PiperOrigin-RevId: 320627333 --- firestore/src/tests/firestore_integration_test.cc | 8 +------- firestore/src/tests/firestore_integration_test.h | 4 ---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/firestore/src/tests/firestore_integration_test.cc b/firestore/src/tests/firestore_integration_test.cc index ec286f6730..7f43c9f13f 100644 --- a/firestore/src/tests/firestore_integration_test.cc +++ b/firestore/src/tests/firestore_integration_test.cc @@ -61,7 +61,6 @@ FirestoreIntegrationTest::FirestoreIntegrationTest() { FirestoreIntegrationTest::~FirestoreIntegrationTest() { for (auto named_firestore : firestores_) { - Await(named_firestore.second->Terminate()); Release(named_firestore.second); firestores_[named_firestore.first] = nullptr; } @@ -108,7 +107,7 @@ void FirestoreIntegrationTest::DeleteFirestore(const std::string& name) { found != firestores_.end(), "Couldn't find Firestore corresponding to app name '%s'", name.c_str()); - TerminateAndRelease(found->second); + Release(found->second); firestores_.erase(found); } @@ -233,10 +232,5 @@ std::string FirestoreIntegrationTest::DescribeFailedFuture( std::to_string(future.error()) + ", message " + future.error_message(); } -void FirestoreIntegrationTest::TerminateAndRelease(Firestore* firestore) { - Await(firestore->Terminate()); - Release(firestore); -} - } // namespace firestore } // namespace firebase diff --git a/firestore/src/tests/firestore_integration_test.h b/firestore/src/tests/firestore_integration_test.h index 79bc23b1cb..9cae76625c 100644 --- a/firestore/src/tests/firestore_integration_test.h +++ b/firestore/src/tests/firestore_integration_test.h @@ -274,10 +274,6 @@ class FirestoreIntegrationTest : public testing::Test { template friend class EventAccumulator; - // Blocks until the given Firestore instance terminates, deletes the instance - // and removes the pointer to it from the cache. - void TerminateAndRelease(Firestore* firestore); - // The Firestore instance cache. mutable std::map firestores_; }; From 0827dad23d746c587f7d7451390166b552bdf1d9 Mon Sep 17 00:00:00 2001 From: chkuang Date: Fri, 10 Jul 2020 14:53:13 -0700 Subject: [PATCH 032/109] Change std:unique_ptr to UniquePtr This is causing Rapid build fail with error message like "error: no template named 'make_unique' in namespace 'std'; did you mean 'MakeUnique'?" Since we have our own implementation, we should use it consistently. PiperOrigin-RevId: 320681249 --- database/src/desktop/database_desktop.cc | 4 ++-- database/src/desktop/database_desktop.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/database/src/desktop/database_desktop.cc b/database/src/desktop/database_desktop.cc index e6290b0937..70ddbbab96 100644 --- a/database/src/desktop/database_desktop.cc +++ b/database/src/desktop/database_desktop.cc @@ -287,8 +287,8 @@ void DatabaseInternal::UnregisterAllChildListeners( void DatabaseInternal::EnsureRepo() { MutexLock lock(repo_mutex_); if (!repo_) { - repo_ = std::make_unique(app_, this, database_url_.c_str(), &logger_, - persistence_enabled_); + repo_ = MakeUnique(app_, this, database_url_.c_str(), &logger_, + persistence_enabled_); } } diff --git a/database/src/desktop/database_desktop.h b/database/src/desktop/database_desktop.h index 84291c5efa..e341de5570 100644 --- a/database/src/desktop/database_desktop.h +++ b/database/src/desktop/database_desktop.h @@ -26,6 +26,7 @@ #include "app/src/mutex.h" #include "app/src/safe_reference.h" #include "app/src/scheduler.h" +#include "app/memory/unique_ptr.h" #include "database/src/common/listener.h" #include "database/src/common/query_spec.h" #include "database/src/desktop/connection/host_info.h" @@ -208,7 +209,7 @@ class DatabaseInternal { Mutex repo_mutex_; // The local copy of the repository, for offline support and local caching. - std::unique_ptr repo_; + UniquePtr repo_; }; } // namespace internal From 082c5d2f25494e0f1b91cfb53e4d8da545909604 Mon Sep 17 00:00:00 2001 From: chkuang Date: Fri, 10 Jul 2020 15:29:53 -0700 Subject: [PATCH 033/109] Update Android deps to latest PiperOrigin-RevId: 320688041 --- Android/firebase_dependencies.gradle | 20 +++++++++---------- admob/admob_resources/build.gradle | 4 ++-- app/app_resources/build.gradle | 2 +- app/google_api_resources/build.gradle | 2 +- app/invites_resources/build.gradle | 2 +- auth/auth_resources/build.gradle | 4 ++-- database/database_resources/build.gradle | 4 ++-- firestore/firestore_resources/build.gradle | 4 ++-- messaging/messaging_java/build.gradle | 4 ++-- .../Android/firebase_dependencies.gradle | 20 +++++++++---------- storage/storage_resources/build.gradle | 2 +- 11 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Android/firebase_dependencies.gradle b/Android/firebase_dependencies.gradle index d1c5689d06..4ae98c201c 100644 --- a/Android/firebase_dependencies.gradle +++ b/Android/firebase_dependencies.gradle @@ -16,22 +16,22 @@ import org.gradle.util.ConfigureUtil; // A map of library to the dependencies that need to be added for it. def firebaseDependenciesMap = [ - 'app' : ['com.google.firebase:firebase-analytics:17.4.1'], - 'admob' : ['com.google.firebase:firebase-ads:19.1.0', - 'com.google.android.gms:play-services-measurement-sdk-api:17.4.1'], - 'analytics' : ['com.google.firebase:firebase-analytics:17.4.1'], - 'auth' : ['com.google.firebase:firebase-auth:19.3.1'], - 'database' : ['com.google.firebase:firebase-database:19.3.0'], + 'app' : ['com.google.firebase:firebase-analytics:17.4.4'], + 'admob' : ['com.google.firebase:firebase-ads:19.2.0', + 'com.google.android.gms:play-services-measurement-sdk-api:17.4.4'], + 'analytics' : ['com.google.firebase:firebase-analytics:17.4.4'], + 'auth' : ['com.google.firebase:firebase-auth:19.3.2'], + 'database' : ['com.google.firebase:firebase-database:19.3.1'], 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links:19.1.0'], - 'firestore' : ['com.google.firebase:firebase-firestore:21.4.3'], + 'firestore' : ['com.google.firebase:firebase-firestore:21.5.0'], 'functions' : ['com.google.firebase:firebase-functions:19.0.2'], - 'instance_id' : ['com.google.firebase:firebase-iid:20.1.7'], + 'instance_id' : ['com.google.firebase:firebase-iid:20.2.3'], 'invites' : ['com.google.firebase:firebase-invites:17.0.0'], // Messaging has an additional local dependency to include. - 'messaging' : ['com.google.firebase:firebase-messaging:20.1.7', + 'messaging' : ['com.google.firebase:firebase-messaging:20.2.3', 'firebase_cpp_sdk.messaging:messaging_java'], 'performance' : ['com.google.firebase:firebase-perf:19.0.7'], - 'remote_config' : ['com.google.firebase:firebase-config:19.1.4'], + 'remote_config' : ['com.google.firebase:firebase-config:19.2.0'], 'storage' : ['com.google.firebase:firebase-storage:19.1.1'] ] diff --git a/admob/admob_resources/build.gradle b/admob/admob_resources/build.gradle index 19982016ae..4597cc77d0 100644 --- a/admob/admob_resources/build.gradle +++ b/admob/admob_resources/build.gradle @@ -45,8 +45,8 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' - implementation 'com.google.firebase:firebase-ads:19.1.0' + implementation 'com.google.firebase:firebase-analytics:17.4.4' + implementation 'com.google.firebase:firebase-ads:19.2.0' } afterEvaluate { diff --git a/app/app_resources/build.gradle b/app/app_resources/build.gradle index 581dc3c59c..f4110eedf5 100644 --- a/app/app_resources/build.gradle +++ b/app/app_resources/build.gradle @@ -46,7 +46,7 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' + implementation 'com.google.firebase:firebase-analytics:17.4.4' } afterEvaluate { diff --git a/app/google_api_resources/build.gradle b/app/google_api_resources/build.gradle index 559b37a315..51b9b180e2 100644 --- a/app/google_api_resources/build.gradle +++ b/app/google_api_resources/build.gradle @@ -49,7 +49,7 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' + implementation 'com.google.firebase:firebase-analytics:17.4.4' implementation project(':app:app_resources') } diff --git a/app/invites_resources/build.gradle b/app/invites_resources/build.gradle index 34f545eeb0..c5db176719 100644 --- a/app/invites_resources/build.gradle +++ b/app/invites_resources/build.gradle @@ -45,7 +45,7 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' + implementation 'com.google.firebase:firebase-analytics:17.4.4' implementation 'com.google.firebase:firebase-dynamic-links:19.1.0' implementation project(':app:app_resources') } diff --git a/auth/auth_resources/build.gradle b/auth/auth_resources/build.gradle index 39000474d6..25e0263958 100644 --- a/auth/auth_resources/build.gradle +++ b/auth/auth_resources/build.gradle @@ -45,8 +45,8 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' - implementation 'com.google.firebase:firebase-auth:19.3.1' + implementation 'com.google.firebase:firebase-analytics:17.4.4' + implementation 'com.google.firebase:firebase-auth:19.3.2' implementation project(':app:app_resources') } diff --git a/database/database_resources/build.gradle b/database/database_resources/build.gradle index 831f2f22fd..b5040464df 100644 --- a/database/database_resources/build.gradle +++ b/database/database_resources/build.gradle @@ -45,8 +45,8 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' - implementation 'com.google.firebase:firebase-database:19.3.0' + implementation 'com.google.firebase:firebase-analytics:17.4.4' + implementation 'com.google.firebase:firebase-database:19.3.1' //implementation project(':app:app_resources') } diff --git a/firestore/firestore_resources/build.gradle b/firestore/firestore_resources/build.gradle index acdc68c932..839d3151a6 100644 --- a/firestore/firestore_resources/build.gradle +++ b/firestore/firestore_resources/build.gradle @@ -46,8 +46,8 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' - implementation 'com.google.firebase:firebase-firestore:21.4.3' + implementation 'com.google.firebase:firebase-analytics:17.4.4' + implementation 'com.google.firebase:firebase-firestore:21.5.0' } afterEvaluate { diff --git a/messaging/messaging_java/build.gradle b/messaging/messaging_java/build.gradle index 0f33cbaa85..0f95391ecb 100644 --- a/messaging/messaging_java/build.gradle +++ b/messaging/messaging_java/build.gradle @@ -51,8 +51,8 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' - implementation 'com.google.firebase:firebase-messaging:20.1.7' + implementation 'com.google.firebase:firebase-analytics:17.4.4' + implementation 'com.google.firebase:firebase-messaging:20.2.3' implementation 'com.google.flatbuffers:flatbuffers-java:1.9.0' } diff --git a/release_build_files/Android/firebase_dependencies.gradle b/release_build_files/Android/firebase_dependencies.gradle index 947faa189d..50717b914f 100644 --- a/release_build_files/Android/firebase_dependencies.gradle +++ b/release_build_files/Android/firebase_dependencies.gradle @@ -16,20 +16,20 @@ import org.gradle.util.ConfigureUtil; // A map of library to the dependencies that need to be added for it. def firebaseDependenciesMap = [ - 'app' : ['com.google.firebase:firebase-analytics:17.4.1'], - 'admob' : ['com.google.firebase:firebase-ads:19.1.0', - 'com.google.android.gms:play-services-measurement-sdk-api:17.4.1'], - 'analytics' : ['com.google.firebase:firebase-analytics:17.4.1'], - 'auth' : ['com.google.firebase:firebase-auth:19.3.1'], - 'database' : ['com.google.firebase:firebase-database:19.3.0'], + 'app' : ['com.google.firebase:firebase-analytics:17.4.4'], + 'admob' : ['com.google.firebase:firebase-ads:19.2.0', + 'com.google.android.gms:play-services-measurement-sdk-api:17.4.4'], + 'analytics' : ['com.google.firebase:firebase-analytics:17.4.4'], + 'auth' : ['com.google.firebase:firebase-auth:19.3.2'], + 'database' : ['com.google.firebase:firebase-database:19.3.1'], 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links:19.1.0'], - 'firestore' : ['com.google.firebase:firebase-firestore:21.4.3'], + 'firestore' : ['com.google.firebase:firebase-firestore:21.5.0'], 'functions' : ['com.google.firebase:firebase-functions:19.0.2'], - 'instance_id' : ['com.google.firebase:firebase-iid:20.1.7'], + 'instance_id' : ['com.google.firebase:firebase-iid:20.2.3'], 'messaging' : ['com.google.firebase.messaging.cpp:firebase_messaging_cpp@aar', - 'com.google.firebase:firebase-messaging:20.1.7'], + 'com.google.firebase:firebase-messaging:20.2.3'], 'performance' : ['com.google.firebase:firebase-perf:19.0.7'], - 'remote_config' : ['com.google.firebase:firebase-config:19.1.4'], + 'remote_config' : ['com.google.firebase:firebase-config:19.2.0'], 'storage' : ['com.google.firebase:firebase-storage:19.1.1'], 'testlab' : [] ] diff --git a/storage/storage_resources/build.gradle b/storage/storage_resources/build.gradle index e065ffa817..bb2d8f4fa9 100644 --- a/storage/storage_resources/build.gradle +++ b/storage/storage_resources/build.gradle @@ -45,7 +45,7 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.1' + implementation 'com.google.firebase:firebase-analytics:17.4.4' implementation 'com.google.firebase:firebase-storage:19.1.1' } From 0490b4b2aadfd96ffc7804093f6d6f6556a447a0 Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 13 Jul 2020 13:32:30 -0700 Subject: [PATCH 034/109] Make test failures in numeric transforms tests easier to read Previously, if the value was of the wrong type, you'd get a message like: Value of: snap.Get("sum").is_integer() Actual: false Expected: true Which would give no indication of what the actual type or value was. Now tests will fail like this: Expected equality of these values: snap.Get("sum") Which is: 1337 (Type::kDouble) FieldValue::Integer(value) Which is: 1337 (Type::kInteger) As an added bonus this also simplifies the calling code because now we can just assert that a value in a snapshot is equal to some expected value and GoogleTest will do the heavy lifting of printing the differences. One unsolved problem with this approach is how to handle equality within epsilon for floating point values. This turns out to be non-trivial without writing custom matchers, which is beyond the scope of what I wanted to tackle here. Instead of solving this I've changed the tests to use values that have an exact representation as doubles. This is easier to do for integral values than for fractional ones so the tests now use integer-valued doubles to achieve the same effect of cumulative addition as before. PiperOrigin-RevId: 321022746 --- .../src/tests/numeric_transforms_test.cc | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/firestore/src/tests/numeric_transforms_test.cc b/firestore/src/tests/numeric_transforms_test.cc index 1dfd404d2b..9840b39885 100644 --- a/firestore/src/tests/numeric_transforms_test.cc +++ b/firestore/src/tests/numeric_transforms_test.cc @@ -9,8 +9,59 @@ namespace firebase { namespace firestore { +using Type = FieldValue::Type; + using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior; +const char* TypeName(Type type) { + switch (type) { + case Type::kNull: + return "kNull"; + case Type::kBoolean: + return "kBoolean"; + case Type::kInteger: + return "kInteger"; + case Type::kDouble: + return "kDouble"; + case Type::kTimestamp: + return "kTimestamp"; + case Type::kString: + return "kString"; + case Type::kBlob: + return "kBlob"; + case Type::kReference: + return "kReference"; + case Type::kGeoPoint: + return "kGeoPoint"; + case Type::kArray: + return "kArray"; + case Type::kMap: + return "kMap"; + case Type::kDelete: + return "kDelete"; + case Type::kServerTimestamp: + return "kServerTimestamp"; + case Type::kArrayUnion: + return "kArrayUnion"; + case Type::kArrayRemove: + return "kArrayRemove"; + case Type::kIncrementInteger: + return "kIncrementInteger"; + case Type::kIncrementDouble: + return "kIncrementDouble"; + } +} + +void PrintTo(const Type& type, std::ostream* os) { + *os << "Type::" << TypeName(type); +} + +void PrintTo(const FieldValue& f, std::ostream* os) { + *os << f.ToString() << " ("; + PrintTo(f.type(), os); + *os << ")"; +} + class NumericTransformsTest : public FirestoreIntegrationTest { public: NumericTransformsTest() { @@ -35,20 +86,16 @@ class NumericTransformsTest : public FirestoreIntegrationTest { void ExpectLocalAndRemoteValue(int value) { DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); - EXPECT_TRUE(snap.Get("sum").is_integer()); - EXPECT_EQ(value, snap.Get("sum").integer_value()); + ASSERT_EQ(snap.Get("sum"), FieldValue::Integer(value)); snap = accumulator_.AwaitRemoteEvent(); - EXPECT_TRUE(snap.Get("sum").is_integer()); - EXPECT_EQ(value, snap.Get("sum").integer_value()); + ASSERT_EQ(snap.Get("sum"), FieldValue::Integer(value)); } void ExpectLocalAndRemoteValue(double value) { DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); - EXPECT_TRUE(snap.Get("sum").is_double()); - EXPECT_DOUBLE_EQ(value, snap.Get("sum").double_value()); + ASSERT_EQ(snap.Get("sum"), FieldValue::Double(value)); snap = accumulator_.AwaitRemoteEvent(); - EXPECT_TRUE(snap.Get("sum").is_double()); - EXPECT_DOUBLE_EQ(value, snap.Get("sum").double_value()); + ASSERT_EQ(snap.Get("sum"), FieldValue::Double(value)); } // A document reference to read and write. @@ -85,27 +132,27 @@ TEST_F(NumericTransformsTest, IntegerIncrementWithExistingInteger) { } TEST_F(NumericTransformsTest, DoubleIncrementWithExistingDouble) { - WriteInitialData({{"sum", FieldValue::Double(13.37)}}); + WriteInitialData({{"sum", FieldValue::Double(0.5)}}); - Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}})); + Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.25)}})); - ExpectLocalAndRemoteValue(13.47); + ExpectLocalAndRemoteValue(0.75); } TEST_F(NumericTransformsTest, IntegerIncrementWithExistingDouble) { - WriteInitialData({{"sum", FieldValue::Double(13.37)}}); + WriteInitialData({{"sum", FieldValue::Double(0.5)}}); Await(doc_ref_.Update({{"sum", FieldValue::Increment(1)}})); - ExpectLocalAndRemoteValue(14.37); + ExpectLocalAndRemoteValue(1.5); } TEST_F(NumericTransformsTest, DoubleIncrementWithExistingInteger) { - WriteInitialData({{"sum", FieldValue::Integer(1337)}}); + WriteInitialData({{"sum", FieldValue::Integer(1)}}); - Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}})); + Await(doc_ref_.Update({{"sum", FieldValue::Increment(0.5)}})); - ExpectLocalAndRemoteValue(1337.1); + ExpectLocalAndRemoteValue(1.5); } TEST_F(NumericTransformsTest, IntegerIncrementWithExistingString) { @@ -119,9 +166,9 @@ TEST_F(NumericTransformsTest, IntegerIncrementWithExistingString) { TEST_F(NumericTransformsTest, DoubleIncrementWithExistingString) { WriteInitialData({{"sum", FieldValue::String("overwrite")}}); - Await(doc_ref_.Update({{"sum", FieldValue::Increment(13.37)}})); + Await(doc_ref_.Update({{"sum", FieldValue::Increment(1.5)}})); - ExpectLocalAndRemoteValue(13.37); + ExpectLocalAndRemoteValue(1.5); } TEST_F(NumericTransformsTest, MultipleDoubleIncrements) { @@ -129,27 +176,24 @@ TEST_F(NumericTransformsTest, MultipleDoubleIncrements) { DisableNetwork(); - doc_ref_.Update({{"sum", FieldValue::Increment(0.1)}}); - doc_ref_.Update({{"sum", FieldValue::Increment(0.01)}}); - doc_ref_.Update({{"sum", FieldValue::Increment(0.001)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(0.5)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(1.0)}}); + doc_ref_.Update({{"sum", FieldValue::Increment(2.0)}}); DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); - EXPECT_TRUE(snap.Get("sum").is_double()); - EXPECT_DOUBLE_EQ(0.1, snap.Get("sum").double_value()); + EXPECT_EQ(snap.Get("sum"), FieldValue::Double(0.5)); snap = accumulator_.AwaitLocalEvent(); - EXPECT_TRUE(snap.Get("sum").is_double()); - EXPECT_DOUBLE_EQ(0.11, snap.Get("sum").double_value()); + EXPECT_EQ(snap.Get("sum"), FieldValue::Double(1.5)); snap = accumulator_.AwaitLocalEvent(); - EXPECT_TRUE(snap.Get("sum").is_double()); - EXPECT_DOUBLE_EQ(0.111, snap.Get("sum").double_value()); + EXPECT_EQ(snap.Get("sum"), FieldValue::Double(3.5)); EnableNetwork(); snap = accumulator_.AwaitRemoteEvent(); - EXPECT_DOUBLE_EQ(0.111, snap.Get("sum").double_value()); + EXPECT_EQ(snap.Get("sum"), FieldValue::Double(3.5)); } TEST_F(NumericTransformsTest, IncrementTwiceInABatch) { @@ -186,12 +230,12 @@ TEST_F(NumericTransformsTest, ServerTimestampAndIncrement) { doc_ref_.Set({{"sum", FieldValue::Increment(1)}}); DocumentSnapshot snapshot = accumulator_.AwaitLocalEvent(); - EXPECT_TRUE( - snapshot.Get("sum", ServerTimestampBehavior::kEstimate).is_timestamp()); + EXPECT_EQ(snapshot.Get("sum", ServerTimestampBehavior::kEstimate).type(), + Type::kTimestamp); DocumentSnapshot snap = accumulator_.AwaitLocalEvent(); EXPECT_TRUE(snap.Get("sum").is_integer()); - EXPECT_EQ(1, snap.Get("sum").integer_value()); + EXPECT_EQ(snap.Get("sum"), FieldValue::Integer(1)); EnableNetwork(); From cb36be524d0600e4f1753df8bde97fdc599d5695 Mon Sep 17 00:00:00 2001 From: mcg Date: Mon, 13 Jul 2020 16:40:27 -0700 Subject: [PATCH 035/109] Add JNI Object wrapper and type conversion traits This is the first in a series of commits that aims to convert our JNI usage to a more modern approach while still remaining STLPort compatible. PiperOrigin-RevId: 321059370 --- .../include/firebase/internal/type_traits.h | 6 ++ firestore/src/jni/object.cc | 15 +++ firestore/src/jni/object.h | 51 ++++++++++ firestore/src/jni/traits.h | 97 +++++++++++++++++++ firestore/src/tests/jni/object_test.cc | 31 ++++++ firestore/src/tests/jni/traits_test.cc | 73 ++++++++++++++ 6 files changed, 273 insertions(+) create mode 100644 firestore/src/jni/object.cc create mode 100644 firestore/src/jni/object.h create mode 100644 firestore/src/jni/traits.h create mode 100644 firestore/src/tests/jni/object_test.cc create mode 100644 firestore/src/tests/jni/traits_test.cc diff --git a/app/src/include/firebase/internal/type_traits.h b/app/src/include/firebase/internal/type_traits.h index 6b31f77206..b9a3718037 100644 --- a/app/src/include/firebase/internal/type_traits.h +++ b/app/src/include/firebase/internal/type_traits.h @@ -85,6 +85,9 @@ struct is_lvalue_reference { template using decay = FIREBASE_TYPE_TRAITS_NS::decay; +template +using decay_t = typename decay::type; + template using enable_if = FIREBASE_TYPE_TRAITS_NS::enable_if; @@ -100,6 +103,9 @@ using is_same = FIREBASE_TYPE_TRAITS_NS::is_same; template using integral_constant = FIREBASE_TYPE_TRAITS_NS::integral_constant; +using true_type = FIREBASE_TYPE_TRAITS_NS::true_type; +using false_type = FIREBASE_TYPE_TRAITS_NS::false_type; + #undef FIREBASE_TYPE_TRAITS_NS // `is_char::value` is true iff `T` is a character type (including `wchar_t` diff --git a/firestore/src/jni/object.cc b/firestore/src/jni/object.cc new file mode 100644 index 0000000000..79daa595f1 --- /dev/null +++ b/firestore/src/jni/object.cc @@ -0,0 +1,15 @@ +#include "firestore/src/jni/object.h" + +#include "app/src/util_android.h" + +namespace firebase { +namespace firestore { +namespace jni { + +std::string Object::ToString(JNIEnv* env) const { + return util::JniObjectToString(env, object_); +} + +} // namespace jni +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/jni/object.h b/firestore/src/jni/object.h new file mode 100644 index 0000000000..8ed5d2bac4 --- /dev/null +++ b/firestore/src/jni/object.h @@ -0,0 +1,51 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_OBJECT_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_OBJECT_H_ + +#include + +#include + +namespace firebase { +namespace firestore { +namespace jni { + +/** + * A wrapper for a JNI `jobject` that adds additional behavior. + * + * `Object` merely holds values with `jobject` types, see `Local` and `Global` + * template subclasses for reference-type-aware wrappers that automatically + * manage the lifetime of JNI objects. + */ +class Object { + public: + Object() = default; + explicit Object(jobject object) : object_(object) {} + virtual ~Object() = default; + + explicit operator bool() const { return object_ != nullptr; } + + virtual jobject get() const { return object_; } + + /** + * Converts this object to a C++ String by calling the Java `toString` method + * on it. + */ + std::string ToString(JNIEnv* env) const; + + protected: + jobject object_ = nullptr; +}; + +inline bool operator==(const Object& lhs, const Object& rhs) { + return lhs.get() == rhs.get(); +} + +inline bool operator!=(const Object& lhs, const Object& rhs) { + return !(lhs == rhs); +} + +} // namespace jni +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_OBJECT_H_ diff --git a/firestore/src/jni/traits.h b/firestore/src/jni/traits.h new file mode 100644 index 0000000000..191dc65bb0 --- /dev/null +++ b/firestore/src/jni/traits.h @@ -0,0 +1,97 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_TRAITS_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_TRAITS_H_ + +#include + +#include "app/src/include/firebase/internal/type_traits.h" + +namespace firebase { +namespace firestore { +namespace jni { + +class Object; + +// clang-format off + +// MARK: Primitive and Reference Type traits + +template struct IsPrimitive : public false_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; +template <> struct IsPrimitive : public true_type {}; + +template struct IsReference : public false_type {}; +template <> struct IsReference : public true_type {}; +template <> struct IsReference : public true_type {}; +template <> struct IsReference : public true_type {}; +template <> struct IsReference : public true_type {}; +template <> struct IsReference : public true_type {}; +template <> struct IsReference : public true_type {}; + + +// MARK: Type mapping + +// A compile-time map from C++ types to their JNI equivalents. +template struct JniTypeMap {}; +template <> struct JniTypeMap { using type = jboolean; }; +template <> struct JniTypeMap { using type = jbyte; }; +template <> struct JniTypeMap { using type = jchar; }; +template <> struct JniTypeMap { using type = jshort; }; +template <> struct JniTypeMap { using type = jint; }; +template <> struct JniTypeMap { using type = jlong; }; +template <> struct JniTypeMap { using type = jfloat; }; +template <> struct JniTypeMap { using type = jdouble; }; +template <> struct JniTypeMap { using type = jsize; }; + +template <> struct JniTypeMap { using type = jobject; }; + +template <> struct JniTypeMap { using type = jobject; }; + +template +using JniType = typename JniTypeMap>::type; + +// clang-format on + +// MARK: Enable If Helpers + +template +using EnableForPrimitive = + typename enable_if>::value, R>::type; + +template +using EnableForReference = + typename enable_if>::value, R>::type; + +// MARK: Type converters + +// Converts C++ primitives to their equivalent JNI primitive types by casting. +template +EnableForPrimitive> ToJni(const T& value) { + return static_cast>(value); +} + +// Converts JNI wrapper reference types (like `const Object&`) and any ownership +// wrappers of those types to their underlying `jobject`-derived reference. +template +EnableForReference> ToJni(const T& value) { + return value.get(); +} +template +J ToJni(const T& value) { + return value.get(); +} + +// Preexisting JNI types can be passed directly. This makes incremental +// migration possible. Ideally this could eventually be removed. +inline jobject ToJni(jobject value) { return value; } + +} // namespace jni +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_TRAITS_H_ diff --git a/firestore/src/tests/jni/object_test.cc b/firestore/src/tests/jni/object_test.cc new file mode 100644 index 0000000000..bd0d594ded --- /dev/null +++ b/firestore/src/tests/jni/object_test.cc @@ -0,0 +1,31 @@ +#include "firestore/src/jni/object.h" + +#include + +#include "firestore/src/tests/firestore_integration_test.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace jni { + +class ObjectTest : public FirestoreIntegrationTest { + public: + ObjectTest() : env_(app()->GetJNIEnv()) {} + + protected: + JNIEnv* env_ = nullptr; +}; + +TEST_F(ObjectTest, ToString) { + jclass string_class = env_->FindClass("java/lang/String"); + Object wrapper(string_class); + + // java.lang.Class defines its toString output as having this form. + EXPECT_EQ("class java.lang.String", wrapper.ToString(env_)); + env_->DeleteLocalRef(string_class); +} + +} // namespace jni +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/tests/jni/traits_test.cc b/firestore/src/tests/jni/traits_test.cc new file mode 100644 index 0000000000..6bb2032df1 --- /dev/null +++ b/firestore/src/tests/jni/traits_test.cc @@ -0,0 +1,73 @@ +#include "firestore/src/jni/traits.h" + +#include + +#include + +#include "firestore/src/jni/object.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace jni { + +using testing::StaticAssertTypeEq; + +using TraitsTest = FirestoreIntegrationTest; + +template +void ExpectConvertsPrimitive() { + static_assert(sizeof(C) >= sizeof(J), + "Java types should never be bigger than C++ equivalents"); + + // Some C++ types (notably size_t) have a size that is not fixed. Use the + // maximum value supported by the Java type for testing. + C cpp_value = static_cast(std::numeric_limits::max()); + J jni_value = ToJni(cpp_value); + EXPECT_EQ(jni_value, static_cast(cpp_value)); +} + +TEST_F(TraitsTest, ConvertsPrimitives) { + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); + ExpectConvertsPrimitive(); +} + +TEST_F(TraitsTest, ConvertsObjects) { + Object cpp_value; + jobject jni_value = ToJni(cpp_value); + EXPECT_EQ(jni_value, nullptr); + + jobject jobject_value = nullptr; + jni_value = ToJni(jobject_value); + EXPECT_EQ(jni_value, nullptr); + + jni_value = ToJni(nullptr); + EXPECT_EQ(jni_value, nullptr); +} + +// Conversion implicitly tests type mapping. Additionally test variations of +// types that should be equivalent. +TEST_F(TraitsTest, DecaysBeforeMappingTypes) { + StaticAssertTypeEq, jint>(); + StaticAssertTypeEq, jint>(); + + StaticAssertTypeEq, jobject>(); + StaticAssertTypeEq, jobject>(); + + StaticAssertTypeEq, jobject>(); + StaticAssertTypeEq, jobject>(); + StaticAssertTypeEq, jobject>(); + StaticAssertTypeEq, jobject>(); +} + +} // namespace jni +} // namespace firestore +} // namespace firebase From 30f9b9e1bc426c07df8a0e1e16f7c752c7231030 Mon Sep 17 00:00:00 2001 From: mcg Date: Tue, 14 Jul 2020 09:42:12 -0700 Subject: [PATCH 036/109] Add JNI ownership types These generate local and global reference subtypes of a given JNI wrapper that make it possible to automatically emit DeleteLocalRef and DeleteGlobalRef calls in the course of regular usage. PiperOrigin-RevId: 321175591 --- firestore/src/android/firestore_android.cc | 3 + firestore/src/jni/jni.cc | 91 ++++++ firestore/src/jni/jni.h | 25 ++ firestore/src/jni/jni_fwd.h | 27 ++ firestore/src/jni/ownership.h | 224 +++++++++++++ firestore/src/jni/traits.h | 3 +- firestore/src/tests/jni/ownership_test.cc | 363 +++++++++++++++++++++ 7 files changed, 734 insertions(+), 2 deletions(-) create mode 100644 firestore/src/jni/jni.cc create mode 100644 firestore/src/jni/jni.h create mode 100644 firestore/src/jni/jni_fwd.h create mode 100644 firestore/src/jni/ownership.h create mode 100644 firestore/src/tests/jni/ownership_test.cc diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index a69f96f5b6..b61e43c6c7 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -38,6 +38,7 @@ #include "firestore/src/android/wrapper.h" #include "firestore/src/android/write_batch_android.h" #include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/jni/jni.h" namespace firebase { namespace firestore { @@ -144,6 +145,8 @@ FirestoreInternal::FirestoreInternal(App* app) { bool FirestoreInternal::Initialize(App* app) { MutexLock init_lock(init_mutex_); if (initialize_count_ == 0) { + jni::Initialize(app->java_vm()); + JNIEnv* env = app->GetJNIEnv(); jobject activity = app->activity(); if (!(firebase_firestore::CacheMethodIds(env, activity) && diff --git a/firestore/src/jni/jni.cc b/firestore/src/jni/jni.cc new file mode 100644 index 0000000000..6f070c9921 --- /dev/null +++ b/firestore/src/jni/jni.cc @@ -0,0 +1,91 @@ +#include "firestore/src/jni/jni.h" + +#include + +#include "app/src/assert.h" + +namespace firebase { +namespace firestore { +namespace jni { +namespace { + +pthread_key_t jni_env_key; + +JavaVM* g_jvm = nullptr; + +/** + * Reinterprets `JNIEnv**` out parameters to `void**` on platforms where that's + * required. + */ +void** EnvOut(JNIEnv** env) { return reinterpret_cast(env); } + +JavaVM* GetGlobalJavaVM() { + // TODO(mcg): Use dlsym to call JNI_GetCreatedJavaVMs. + FIREBASE_ASSERT_MESSAGE( + g_jvm != nullptr, + "Global JVM is unset; missing call to jni::Initialize()"); + return g_jvm; +} + +/** + * A callback used by `pthread_key_create` to clean up thread-specific storage + * when a thread is destroyed. + */ +void DetachCurrentThread(void* env) { + JavaVM* jvm = g_jvm; + if (jvm == nullptr || env == nullptr) { + return; + } + + jint result = jvm->DetachCurrentThread(); + if (result != JNI_OK) { + LogWarning("DetachCurrentThread failed to detach (result=%d)", result); + } +} + +} // namespace + +void Initialize(JavaVM* jvm) { + g_jvm = jvm; + + static pthread_once_t initialized = PTHREAD_ONCE_INIT; + pthread_once(&initialized, [] { + int err = pthread_key_create(&jni_env_key, DetachCurrentThread); + FIREBASE_ASSERT_MESSAGE(err == 0, "pthread_key_create failed (errno=%d)", + err); + }); +} + +JNIEnv* GetEnv() { + JavaVM* jvm = GetGlobalJavaVM(); + + JNIEnv* env = nullptr; + jint result = jvm->GetEnv(EnvOut(&env), JNI_VERSION_1_6); + if (result == JNI_OK) { + // Called from a JVM-managed thread or from a thread that was previously + // attached. In either case, there's no work to be done. + return env; + } + + // The only other documented error is JNI_EVERSION, but all supported Android + // implementations support JNI 1.6 so this shouldn't happen. + FIREBASE_ASSERT_MESSAGE(result == JNI_EDETACHED, + "GetEnv failed with an unexpected error (result=%d)", + result); + + // If we've gotten here, the current thread is a native thread that has not + // been attached, so we need to attach it and set up a thread-local + // destructor. + result = jvm->AttachCurrentThread(&env, nullptr); + FIREBASE_ASSERT_MESSAGE(result == JNI_OK, + "JNI AttachCurrentThread failed (result=%d)", result); + + result = pthread_setspecific(jni_env_key, env); + FIREBASE_ASSERT_MESSAGE(result == 0, + "JNI pthread_setspecific failed (errno=%d)", result); + return env; +} + +} // namespace jni +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/jni/jni.h b/firestore/src/jni/jni.h new file mode 100644 index 0000000000..fcbcfc408b --- /dev/null +++ b/firestore/src/jni/jni.h @@ -0,0 +1,25 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_H_ + +#include + +namespace firebase { +namespace firestore { +namespace jni { + +/** + * Initializes the global `JavaVM` pointer. Should be called once per process + * execution. + */ +void Initialize(JavaVM* vm); + +/** + * Returns the `JNIEnv` pointer for the current thread. + */ +JNIEnv* GetEnv(); + +} // namespace jni +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_H_ diff --git a/firestore/src/jni/jni_fwd.h b/firestore/src/jni/jni_fwd.h new file mode 100644 index 0000000000..9688b6eae7 --- /dev/null +++ b/firestore/src/jni/jni_fwd.h @@ -0,0 +1,27 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_FWD_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_FWD_H_ + +namespace firebase { +namespace firestore { +namespace jni { + +/** + * Returns the `JNIEnv` pointer for the current thread. + */ +JNIEnv* GetEnv(); + +// Reference types +template +class Local; +template +class Global; +template +class NonOwning; + +class Object; + +} // namespace jni +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_FWD_H_ diff --git a/firestore/src/jni/ownership.h b/firestore/src/jni/ownership.h new file mode 100644 index 0000000000..aa6292c551 --- /dev/null +++ b/firestore/src/jni/ownership.h @@ -0,0 +1,224 @@ +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_OWNERSHIP_H_ +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_OWNERSHIP_H_ + +#include + +#include + +#include "firestore/src/jni/jni_fwd.h" +#include "firestore/src/jni/traits.h" + +namespace firebase { +namespace firestore { +namespace jni { + +/** + * An RAII wrapper for a local JNI reference that automatically deletes the JNI + * local reference when it goes out of scope. Copies and moves are handled by + * creating additional references as required. + * + * @tparam T A JNI reference wrapper, usually a subclass of `Object`. + */ +template +class Local : public T { + public: + using jni_type = JniType; + + Local() = default; + + /** + * Adopts a local reference that is the result of a JNI invocation. + */ + Local(JNIEnv* env, jni_type value) : T(value), env_(env) {} + + Local(const Local& other) = delete; + Local& operator=(const Local&) = delete; + + Local(Local&& other) noexcept : T(other.release()), env_(other.env_) {} + + Local& operator=(Local&& other) noexcept { + if (T::object_ != other.get()) { + EnsureEnv(other.env()); + env_->DeleteLocalRef(T::object_); + T::object_ = other.release(); + } + return *this; + } + + Local(const Global& other) { + EnsureEnv(); + T::object_ = env_->NewLocalRef(other.get()); + } + + Local& operator=(const Global& other) { + EnsureEnv(); + env_->DeleteLocalRef(T::object_); + T::object_ = env_->NewLocalRef(other.get()); + return *this; + } + + Local(Global&& other) noexcept { + EnsureEnv(); + T::object_ = env_->NewLocalRef(other.get()); + env_->DeleteGlobalRef(other.release()); + } + + Local& operator=(Global&& other) noexcept { + EnsureEnv(); + env_->DeleteLocalRef(T::object_); + T::object_ = env_->NewLocalRef(other.get()); + env_->DeleteGlobalRef(other.release()); + return *this; + } + + ~Local() { + if (env_ && T::object_) { + env_->DeleteLocalRef(T::object_); + } + } + + jni_type get() const override { return static_cast(T::object_); } + + jni_type release() { + jobject result = T::object_; + T::object_ = nullptr; + return static_cast(result); + } + + JNIEnv* env() const { return env_; } + + private: + void EnsureEnv(JNIEnv* other = nullptr) { + if (env_ == nullptr) { + if (other != nullptr) { + env_ = other; + } else { + env_ = GetEnv(); + } + } + } + + JNIEnv* env_ = nullptr; +}; + +/** + * Global references are almost always created by promoting local references. + * Aside from `NewGlobalRef`, there are no JNI APIs that return global + * references. You can construct a `Global` wrapper with `AdoptExisting::kYes` + * in the rare case that you're interoperating with other APIs that produce + * global JNI references. + */ +enum class AdoptExisting { kYes }; + +/** + * An RAII wrapper for a global JNI reference, that automatically deletes the + * JNI global reference when it goes out of scope. Copies and moves are handled + * by creating additional references as required. + * + * @tparam T A JNI reference wrapper, usually a subclass of `Object`. + */ +template +class Global : public T { + public: + using jni_type = JniType; + + Global() = default; + + Global(jni_type object, AdoptExisting) : T(object) {} + + Global(const Local& other) { + JNIEnv* env = EnsureEnv(other.env()); + T::object_ = env->NewGlobalRef(other.get()); + } + + Global& operator=(const Local& other) { + JNIEnv* env = EnsureEnv(other.env()); + env->DeleteGlobalRef(T::object_); + T::object_ = env->NewGlobalRef(other.get()); + return *this; + } + + Global(const T& other) { + JNIEnv* env = GetEnv(); + T::object_ = env->NewGlobalRef(other.get()); + } + + Global& operator=(const T& other) { + if (T::object_ != other.get()) { + JNIEnv* env = GetEnv(); + env->DeleteGlobalRef(T::object_); + T::object_ = env->NewGlobalRef(other.get()); + } + return *this; + } + + // Explicitly declare a copy constructor and copy assignment operator because + // there's an explicitly declared move constructor for this type. + // + // Without this, the implicitly-defined copy constructor would be deleted, and + // during overload resolution the deleted copy constructor would take priority + // over the looser match above that takes `const T&`. + Global(const Global& other) : Global(static_cast(other)) {} + + Global& operator=(const Global& other) { + *this = static_cast(other); + return *this; + } + + Global(Global&& other) noexcept : T(other.release()) {} + + Global& operator=(Global&& other) noexcept { + if (T::object_ != other.get()) { + if (T::object_) { + JNIEnv* env = GetEnv(); + env->DeleteGlobalRef(T::object_); + } + T::object_ = other.release(); + } + return *this; + } + + Global(Local&& other) noexcept { + JNIEnv* env = EnsureEnv(other.env()); + T::object_ = env->NewGlobalRef(other.get()); + env->DeleteLocalRef(other.release()); + } + + Global& operator=(Local&& other) noexcept { + JNIEnv* env = EnsureEnv(other.env()); + env->DeleteGlobalRef(T::object_); + T::object_ = env->NewGlobalRef(other.get()); + env->DeleteLocalRef(other.release()); + return *this; + } + + ~Global() { + if (T::object_) { + JNIEnv* env = GetEnv(); + env->DeleteGlobalRef(T::object_); + } + } + + jni_type get() const override { return static_cast(T::object_); } + + jni_type release() { + jobject result = T::object_; + T::object_ = nullptr; + return static_cast(result); + } + + private: + JNIEnv* EnsureEnv(JNIEnv* other = nullptr) { + if (other != nullptr) { + return other; + } else { + return GetEnv(); + } + } +}; + +} // namespace jni +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_OWNERSHIP_H_ diff --git a/firestore/src/jni/traits.h b/firestore/src/jni/traits.h index 191dc65bb0..4a07295d13 100644 --- a/firestore/src/jni/traits.h +++ b/firestore/src/jni/traits.h @@ -4,13 +4,12 @@ #include #include "app/src/include/firebase/internal/type_traits.h" +#include "firestore/src/jni/jni_fwd.h" namespace firebase { namespace firestore { namespace jni { -class Object; - // clang-format off // MARK: Primitive and Reference Type traits diff --git a/firestore/src/tests/jni/ownership_test.cc b/firestore/src/tests/jni/ownership_test.cc new file mode 100644 index 0000000000..14797ba91a --- /dev/null +++ b/firestore/src/tests/jni/ownership_test.cc @@ -0,0 +1,363 @@ +#include "firestore/src/jni/ownership.h" + +#include + +#include "app/memory/unique_ptr.h" +#include "firestore/src/jni/jni.h" +#include "firestore/src/jni/object.h" +#include "firestore/src/jni/traits.h" +#include "firestore/src/tests/firestore_integration_test.h" +#include "testing/base/public/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace jni { +namespace { + +using testing::Contains; +using testing::Mock; +using testing::Not; +using testing::Return; +using testing::UnorderedElementsAreArray; + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "MemberFunctionCanBeStatic" + +/** + * Tracks live local and global references created through the use of JNIEnv + * by patching the function table in the JNIEnv object. When RefTracker goes + * out of scope, it automatically unpatches the JNIEnv, to avoid affecting any + * tests that follow. + */ +class RefTracker { + public: + RefTracker() { + // Disallow `nullptr` from ever being a valid reference. This prevents bugs + // in the reference wrappers arising from accidentally asserting that + // nullptr is a valid live reference. + invalid_refs_.insert(nullptr); + + instance_ = this; + MaybePatchFunctions(); + } + + ~RefTracker() { + MaybeUnpatchFunctions(); + instance_ = nullptr; + } + + /** + * Creates a new local reference to an arbitrary Java object. + */ + jobject NewLocalObject() { + JNIEnv* env = GetEnv(); + jobject object = env->NewStringUTF("fake"); + EXPECT_NE(object, nullptr); + return object; + } + + /** + * Creates a new global reference to an arbitrary Java object. + */ + jobject NewGlobalObject() { + JNIEnv* env = GetEnv(); + jobject local = env->NewStringUTF("fake"); + jobject global = env->NewGlobalRef(local); + env->DeleteLocalRef(local); + EXPECT_NE(global, nullptr); + return global; + } + + /** + * Makes an assertion that the given objects constitute the set of live JNI + * object references. The objects can be any type convertible to a JNI object + * reference type with `ToJni`. + */ + template + void ExpectLiveIsExactly(Objects&&... objects) { + std::set refs = {ToJni(std::forward(objects))...}; + EXPECT_THAT(refs, testing::ContainerEq(valid_refs_)); + for (jobject ref : refs) { + EXPECT_THAT(invalid_refs_, Not(Contains(ref))); + } + } + + /** + * Makes an assertion that all the given objects have a null value. These + * objects can be any type convertible to a JNI object reference with `ToJni`. + * + * This is largely only useful for verifying the default or moved-from states + * of reference wrapper types. + */ + template + void ExpectNull(Objects&&... objects) { + std::vector refs = {ToJni(std::forward(objects))...}; + for (jobject ref : refs) { + EXPECT_EQ(ref, nullptr); + } + } + + private: + /** + * Patches the function table of the current thread's JNIEnv instance, saving + * aside the current function table in `old_functions_`. + */ + void MaybePatchFunctions() { + JNIEnv* env = GetEnv(); + if (env->functions->NewStringUTF == &PatchedNewStringUTF) return; + + patched_ = true; + old_functions_ = env->functions; + functions_ = *env->functions; + + // Patch functions + functions_.NewGlobalRef = &PatchedNewGlobalRef; + functions_.NewLocalRef = &PatchedNewLocalRef; + functions_.NewStringUTF = &PatchedNewStringUTF; + functions_.DeleteGlobalRef = &PatchedDeleteGlobalRef; + functions_.DeleteLocalRef = &PatchedDeleteLocalRef; + env->functions = &functions_; + } + + /** + * Restores the current thread's JNIEnv instance to its former state, only if + * it was patched by `MaybePatchFunctions`. + */ + void MaybeUnpatchFunctions() { + if (!patched_) return; + + JNIEnv* env = GetEnv(); + if (env->functions->NewStringUTF != &PatchedNewStringUTF) return; + + env->functions = old_functions_; + } + + static jobject MarkValid(jobject object) { + if (object != nullptr) { + instance_->valid_refs_.insert(object); + instance_->invalid_refs_.erase(object); + } + return object; + } + + static void MarkInvalid(jobject object) { + instance_->valid_refs_.erase(object); + instance_->invalid_refs_.insert(object); + } + + static jobject PatchedNewGlobalRef(JNIEnv* env, jobject object) { + jobject result = instance_->old_functions_->NewGlobalRef(env, object); + return MarkValid(result); + } + + static jobject PatchedNewLocalRef(JNIEnv* env, jobject object) { + jobject result = instance_->old_functions_->NewLocalRef(env, object); + return MarkValid(result); + } + + static jstring PatchedNewStringUTF(JNIEnv* env, const char* chars) { + jstring result = instance_->old_functions_->NewStringUTF(env, chars); + EXPECT_NE(result, nullptr); + MarkValid(result); + return result; + } + + static void PatchedDeleteGlobalRef(JNIEnv* env, jobject object) { + MarkInvalid(object); + instance_->old_functions_->DeleteGlobalRef(env, object); + } + + static void PatchedDeleteLocalRef(JNIEnv* env, jobject object) { + MarkInvalid(object); + instance_->old_functions_->DeleteLocalRef(env, object); + } + + std::set valid_refs_; + std::set invalid_refs_; + + bool patched_ = false; + JNINativeInterface functions_; + const JNINativeInterface* old_functions_ = nullptr; + + static RefTracker* instance_; +}; + +#pragma clang diagnostic pop + +RefTracker* RefTracker::instance_ = nullptr; + +class OwnershipTest : public FirestoreIntegrationTest { + public: + OwnershipTest() : env_(GetEnv()) {} + + ~OwnershipTest() override { refs_.ExpectLiveIsExactly(); } + + protected: + JNIEnv* env_ = nullptr; + RefTracker refs_; +}; + +// Local(JNIEnv*, jobject) adopts a local reference returned by JNI so it +// should not call NewLocalRef +TEST_F(OwnershipTest, LocalDeletes) { + jobject local_java = refs_.NewLocalObject(); + { + Local local(env_, local_java); + refs_.ExpectLiveIsExactly(local); + } + refs_.ExpectLiveIsExactly(); +} + +TEST_F(OwnershipTest, LocalReleaseDoesNotDelete) { + jobject local_java = refs_.NewLocalObject(); + { + Local local(env_, local_java); + refs_.ExpectLiveIsExactly(local); + EXPECT_EQ(local_java, local.release()); + } + refs_.ExpectLiveIsExactly(local_java); + env_->DeleteLocalRef(local_java); +} + +TEST_F(OwnershipTest, LocalAcceptsNullptr) { + Local local(env_, nullptr); + EXPECT_EQ(local.get(), nullptr); +} + +TEST_F(OwnershipTest, GlobalCopyFromLocal) { + Local local(env_, refs_.NewLocalObject()); + { + Global global(local); + refs_.ExpectLiveIsExactly(local, global); + } + refs_.ExpectLiveIsExactly(local); +} + +TEST_F(OwnershipTest, GlobalCopyFromDefaultConstructedLocal) { + Local local; + Global global(local); + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, GlobalCopyAssignFromLocal) { + Local local(env_, refs_.NewLocalObject()); + { + Global global; + global = local; + refs_.ExpectLiveIsExactly(local, global); + } + refs_.ExpectLiveIsExactly(local); +} + +TEST_F(OwnershipTest, GlobalCopyAssignFromDefaultConstructedLocal) { + Local local; + Global global; + global = local; + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, GlobalMoveFromLocal) { + Local local(env_, refs_.NewLocalObject()); + { + Global global(Move(local)); + refs_.ExpectLiveIsExactly(global); + } +} + +TEST_F(OwnershipTest, GlobalMoveFromDefaultConstructedLocal) { + Local local; + Global global(Move(local)); + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, GlobalMoveAssignFromLocal) { + Local local(env_, refs_.NewLocalObject()); + { + Global global; + global = Move(local); + refs_.ExpectLiveIsExactly(global); + } +} + +TEST_F(OwnershipTest, GlobalMoveAssignFromDefaultConstructedLocal) { + Local local; + Global global; + global = Move(local); + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, GlobalImplicitMoveAssignFromLocal) { + { + Global global = Local(env_, refs_.NewLocalObject()); + refs_.ExpectLiveIsExactly(global); + } + refs_.ExpectLiveIsExactly(); +} + +TEST_F(OwnershipTest, LocalCopyFromGlobal) { + Global global(refs_.NewGlobalObject(), AdoptExisting::kYes); + { + Local local(global); + refs_.ExpectLiveIsExactly(local, global); + } + refs_.ExpectLiveIsExactly(global); +} + +TEST_F(OwnershipTest, LocalCopyFromDefaultConstructedGlobal) { + Global global; + Local local(global); + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, LocalCopyAssignFromGlobal) { + Global global(refs_.NewGlobalObject(), AdoptExisting::kYes); + { + Local local; + local = global; + refs_.ExpectLiveIsExactly(local, global); + } + refs_.ExpectLiveIsExactly(global); +} + +TEST_F(OwnershipTest, LocalCopyAssignFromDefaultConstructedGlobal) { + Global global; + Local local; + local = global; + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, LocalMoveGlobal) { + Global global(refs_.NewGlobalObject(), AdoptExisting::kYes); + { + Local local(Move(global)); + refs_.ExpectLiveIsExactly(local); + } +} + +TEST_F(OwnershipTest, LocalMoveFromDefaultConstructedGlobal) { + Global global; + Local local(Move(global)); + refs_.ExpectNull(local, global); +} + +TEST_F(OwnershipTest, LocalMoveAssignFromGlobal) { + Global global(refs_.NewGlobalObject(), AdoptExisting::kYes); + { + Local local; + local = Move(global); + refs_.ExpectLiveIsExactly(local); + } +} + +TEST_F(OwnershipTest, LocalMoveAssignFromDefaultConstructedGlobal) { + Global global; + Local local; + local = Move(global); + refs_.ExpectNull(local, global); +} + +} // namespace +} // namespace jni +} // namespace firestore +} // namespace firebase From cdec053a565c9a0fc600a3765c026024efc14a92 Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 14 Jul 2020 10:19:47 -0700 Subject: [PATCH 037/109] Add FIREventScreenView and params PiperOrigin-RevId: 321183165 --- analytics/ios_headers/FIREventNames.h | 9 +++++++++ analytics/ios_headers/FIRParameterNames.h | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/analytics/ios_headers/FIREventNames.h b/analytics/ios_headers/FIREventNames.h index d9bbe7095b..9458782579 100644 --- a/analytics/ios_headers/FIREventNames.h +++ b/analytics/ios_headers/FIREventNames.h @@ -263,6 +263,15 @@ static NSString *const kFIREventPurchaseRefund NS_SWIFT_NAME(AnalyticsEventPurch static NSString *const kFIREventRemoveFromCart NS_SWIFT_NAME(AnalyticsEventRemoveFromCart) = @"remove_from_cart"; +/// Screen View event. This event signifies a screen view. Use this when a screen transition occurs. +/// This event can be logged irrespective of whether automatic screen tracking is enabled. Params: +/// +///
      +///
    • @c kFIRParameterScreenClass (NSString) (optional)
    • +///
    • @c kFIRParameterScreenName (NSString) (optional)
    • +///
    +static NSString *const kFIREventScreenView NS_SWIFT_NAME(AnalyticsEventScreenView) = @"screen_view"; + /// Search event. Apps that support search features can use this event to contextualize search /// operations by supplying the appropriate, corresponding parameters. This event can help you /// identify the most popular content in your app. Params: diff --git a/analytics/ios_headers/FIRParameterNames.h b/analytics/ios_headers/FIRParameterNames.h index 3432044517..515232e9b5 100644 --- a/analytics/ios_headers/FIRParameterNames.h +++ b/analytics/ios_headers/FIRParameterNames.h @@ -385,6 +385,26 @@ static NSString *const kFIRParameterQuantity NS_SWIFT_NAME(AnalyticsParameterQua /// static NSString *const kFIRParameterScore NS_SWIFT_NAME(AnalyticsParameterScore) = @"score"; +/// Current screen class, such as the class name of the UIViewController, logged with screen_view +/// event and added to every event (NSString).
    +///     NSDictionary *params = @{
    +///       kFIRParameterScreenClass : @"LoginViewController",
    +///       // ...
    +///     };
    +/// 
    +static NSString *const kFIRParameterScreenClass NS_SWIFT_NAME(AnalyticsParameterScreenClass) = + @"screen_class"; + +/// Current screen name, such as the name of the UIViewController, logged with screen_view event and +/// added to every event (NSString).
    +///     NSDictionary *params = @{
    +///       kFIRParameterScreenName : @"LoginView",
    +///       // ...
    +///     };
    +/// 
    +static NSString *const kFIRParameterScreenName NS_SWIFT_NAME(AnalyticsParameterScreenName) = + @"screen_name"; + /// The search string/keywords used (NSString). ///
     ///     NSDictionary *params = @{
    
    From 44dbea43e10c23a71e42c3f6e486482c0cc68646 Mon Sep 17 00:00:00 2001
    From: amaurice 
    Date: Tue, 14 Jul 2020 17:50:29 -0700
    Subject: [PATCH 038/109] Add another line in the generate constant header
     script
    
    The generated headers are currently missing a newline prior to @ifdef cpp_examples which is causing doc generation to fail.
    
    PiperOrigin-RevId: 321272103
    ---
     analytics/generate_constants.py | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/analytics/generate_constants.py b/analytics/generate_constants.py
    index 6f9f61616d..a5b984d70a 100755
    --- a/analytics/generate_constants.py
    +++ b/analytics/generate_constants.py
    @@ -91,6 +91,7 @@
         (r'(?s)(@code)([^{].*using namespace firebase::analytics;'
          r'.*Parameter [^{]+[^}]+}.*@endcode)',
          '\n'
    +     '///\n'
          '/// @if cpp_examples\n'
          '/// \\1{.cpp}'
          '\\2\n\n'
    
    From c4731f478dceead932bfc8e505eda0bdbbe539c9 Mon Sep 17 00:00:00 2001
    From: mcg 
    Date: Wed, 15 Jul 2020 17:58:43 -0700
    Subject: [PATCH 039/109] Add initial Env with minimal support for Strings
    
    PiperOrigin-RevId: 321475293
    ---
     firestore/src/jni/env.cc            | 43 ++++++++++++++
     firestore/src/jni/env.h             | 87 +++++++++++++++++++++++++++++
     firestore/src/jni/jni_fwd.h         |  3 +
     firestore/src/jni/object.cc         |  3 +
     firestore/src/jni/object.h          |  3 +
     firestore/src/jni/string.cc         | 17 ++++++
     firestore/src/jni/string.h          | 33 +++++++++++
     firestore/src/jni/traits.h          |  2 +
     firestore/src/tests/jni/env_test.cc | 54 ++++++++++++++++++
     9 files changed, 245 insertions(+)
     create mode 100644 firestore/src/jni/env.cc
     create mode 100644 firestore/src/jni/env.h
     create mode 100644 firestore/src/jni/string.cc
     create mode 100644 firestore/src/jni/string.h
     create mode 100644 firestore/src/tests/jni/env_test.cc
    
    diff --git a/firestore/src/jni/env.cc b/firestore/src/jni/env.cc
    new file mode 100644
    index 0000000000..62bf9177f5
    --- /dev/null
    +++ b/firestore/src/jni/env.cc
    @@ -0,0 +1,43 @@
    +#include "firestore/src/jni/env.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +Local Env::NewStringUtf(const char* bytes) {
    +  if (!ok()) return {};
    +
    +  jstring result = env_->NewStringUTF(bytes);
    +  RecordException();
    +  return Local(env_, result);
    +}
    +
    +std::string Env::GetStringUtfRegion(jstring string, size_t start, size_t len) {
    +  if (!ok()) return "";
    +
    +  // Copy directly into the std::string buffer. This is guaranteed to work as
    +  // of C++11, and also happens to work with STLPort.
    +  std::string result;
    +  result.resize(len);
    +
    +  env_->GetStringUTFRegion(string, ToJni(start), ToJni(len), &result[0]);
    +  RecordException();
    +
    +  // Ensure that if there was an exception, the contents of the buffer are
    +  // disregarded.
    +  if (!ok()) return "";
    +  return result;
    +}
    +
    +void Env::RecordException() {
    +  if (last_exception_ || !env_->ExceptionCheck()) return;
    +
    +  env_->ExceptionDescribe();
    +
    +  last_exception_ = env_->ExceptionOccurred();
    +  env_->ExceptionClear();
    +}
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    diff --git a/firestore/src/jni/env.h b/firestore/src/jni/env.h
    new file mode 100644
    index 0000000000..c5020a8533
    --- /dev/null
    +++ b/firestore/src/jni/env.h
    @@ -0,0 +1,87 @@
    +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ENV_H_
    +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ENV_H_
    +
    +#include 
    +
    +#include 
    +
    +#include "firestore/src/jni/object.h"
    +#include "firestore/src/jni/ownership.h"
    +#include "firestore/src/jni/string.h"
    +#include "firestore/src/jni/traits.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +/**
    + * A wrapper around a JNIEnv pointer that makes dealing with JNI simpler in C++,
    + * by:
    + *
    + *   * Automatically converting arguments of C++ types to their JNI equivalents;
    + *   * handling C++ strings naturally;
    + *   * wrapping JNI references in `Local` RAII wrappers automatically; and
    + *   * simplifying error handling related to JNI calls (see below).
    + *
    + * Normally JNI requires that each call be followed by an explicit check to see
    + * if an exception happened. This is tedious and clutters the code. Instead,
    + * `Env` automatically checks for a JNI exception and short circuits any further
    + * calls. This means that JNI-intensive code can be written straightforwardly
    + * with a single, final check for errors. Exceptions can still be handled
    + * inline if required.
    + */
    +class Env {
    + public:
    +  Env() : env_(GetEnv()) {}
    +
    +  explicit Env(JNIEnv* env) : env_(env) {}
    +
    +  /** Returns true if the Env has not encountered an exception. */
    +  bool ok() const { return last_exception_ == nullptr; }
    +
    +  /** Returns the underlying JNIEnv pointer. */
    +  JNIEnv* get() const { return env_; }
    +
    +  // MARK: String Operations
    +
    +  /**
    +   * Creates a new proxy for a Java String from a sequences of modified UTF-8
    +   * bytes.
    +   */
    +  Local NewStringUtf(const char* bytes);
    +  Local NewStringUtf(const std::string& bytes) {
    +    return NewStringUtf(bytes.c_str());
    +  }
    +
    +  /** Returns the length of the string in modified UTF-8 bytes. */
    +  size_t GetStringUtfLength(jstring string) {
    +    jsize result = env_->GetStringUTFLength(string);
    +    RecordException();
    +    return static_cast(result);
    +  }
    +  size_t GetStringUtfLength(const String& string) {
    +    return GetStringUtfLength(string.get());
    +  }
    +
    +  /**
    +   * Copies the contents of a region of a Java string to a C++ string. The
    +   * resulting string has a modified UTF-8 encoding.
    +   */
    +  std::string GetStringUtfRegion(jstring string, size_t start, size_t len);
    +  std::string GetStringUtfRegion(const String& string, size_t start,
    +                                 size_t len) {
    +    return GetStringUtfRegion(string.get(), start, len);
    +  }
    +
    + private:
    +  void RecordException();
    +
    +  JNIEnv* env_ = nullptr;
    +  jthrowable last_exception_ = nullptr;
    +};
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    +
    +#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ENV_H_
    diff --git a/firestore/src/jni/jni_fwd.h b/firestore/src/jni/jni_fwd.h
    index 9688b6eae7..9ca32b09ce 100644
    --- a/firestore/src/jni/jni_fwd.h
    +++ b/firestore/src/jni/jni_fwd.h
    @@ -10,6 +10,8 @@ namespace jni {
      */
     JNIEnv* GetEnv();
     
    +class Env;
    +
     // Reference types
     template 
     class Local;
    @@ -19,6 +21,7 @@ template 
     class NonOwning;
     
     class Object;
    +class String;
     
     }  // namespace jni
     }  // namespace firestore
    diff --git a/firestore/src/jni/object.cc b/firestore/src/jni/object.cc
    index 79daa595f1..6cd649374b 100644
    --- a/firestore/src/jni/object.cc
    +++ b/firestore/src/jni/object.cc
    @@ -1,6 +1,7 @@
     #include "firestore/src/jni/object.h"
     
     #include "app/src/util_android.h"
    +#include "firestore/src/jni/env.h"
     
     namespace firebase {
     namespace firestore {
    @@ -10,6 +11,8 @@ std::string Object::ToString(JNIEnv* env) const {
       return util::JniObjectToString(env, object_);
     }
     
    +std::string Object::ToString(Env& env) const { return ToString(env.get()); }
    +
     }  // namespace jni
     }  // namespace firestore
     }  // namespace firebase
    diff --git a/firestore/src/jni/object.h b/firestore/src/jni/object.h
    index 8ed5d2bac4..a7b4f05fd6 100644
    --- a/firestore/src/jni/object.h
    +++ b/firestore/src/jni/object.h
    @@ -9,6 +9,8 @@ namespace firebase {
     namespace firestore {
     namespace jni {
     
    +class Env;
    +
     /**
      * A wrapper for a JNI `jobject` that adds additional behavior.
      *
    @@ -31,6 +33,7 @@ class Object {
        * on it.
        */
       std::string ToString(JNIEnv* env) const;
    +  std::string ToString(Env& env) const;
     
      protected:
       jobject object_ = nullptr;
    diff --git a/firestore/src/jni/string.cc b/firestore/src/jni/string.cc
    new file mode 100644
    index 0000000000..088ea68a83
    --- /dev/null
    +++ b/firestore/src/jni/string.cc
    @@ -0,0 +1,17 @@
    +#include "firestore/src/jni/string.h"
    +
    +#include "firestore/src/jni/env.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +std::string String::ToString(Env& env) const {
    +  jstring str = get();
    +  size_t len = env.GetStringUtfLength(str);
    +  return env.GetStringUtfRegion(str, 0, len);
    +}
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    diff --git a/firestore/src/jni/string.h b/firestore/src/jni/string.h
    new file mode 100644
    index 0000000000..8ddc5fe8cc
    --- /dev/null
    +++ b/firestore/src/jni/string.h
    @@ -0,0 +1,33 @@
    +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_STRING_H_
    +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_STRING_H_
    +
    +#include "firestore/src/jni/object.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +/**
    + * A wrapper for a JNI `jstring` that adds additional behavior. This is a proxy
    + * for a Java String in the JVM.
    + *
    + * `String` merely holds values with `jstring` type, see `Local` and `Global`
    + * template subclasses for reference-type-aware wrappers that automatically
    + * manage the lifetime of JNI objects.
    + */
    +class String : public Object {
    + public:
    +  String() = default;
    +  explicit String(jstring string) : Object(string) {}
    +
    +  jstring get() const override { return static_cast(object_); }
    +
    +  /** Converts this Java String to a C++ string. */
    +  std::string ToString(Env& env) const;
    +};
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    +
    +#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_STRING_H_
    diff --git a/firestore/src/jni/traits.h b/firestore/src/jni/traits.h
    index 4a07295d13..99992ed653 100644
    --- a/firestore/src/jni/traits.h
    +++ b/firestore/src/jni/traits.h
    @@ -48,8 +48,10 @@ template <> struct JniTypeMap { using type = jdouble; };
     template <> struct JniTypeMap { using type = jsize; };
     
     template <> struct JniTypeMap { using type = jobject; };
    +template <> struct JniTypeMap { using type = jstring; };
     
     template <> struct JniTypeMap { using type = jobject; };
    +template <> struct JniTypeMap { using type = jstring; };
     
     template 
     using JniType = typename JniTypeMap>::type;
    diff --git a/firestore/src/tests/jni/env_test.cc b/firestore/src/tests/jni/env_test.cc
    new file mode 100644
    index 0000000000..eeb76311f7
    --- /dev/null
    +++ b/firestore/src/tests/jni/env_test.cc
    @@ -0,0 +1,54 @@
    +#include "firestore/src/jni/env.h"
    +
    +#include "firestore/src/tests/firestore_integration_test.h"
    +#include "gtest/gtest.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +class EnvTest : public FirestoreIntegrationTest {
    + public:
    +  EnvTest() : env_(GetEnv()) {}
    +
    + protected:
    +  Env env_;
    +};
    +
    +#if __cpp_exceptions
    +TEST_F(EnvTest, ToolchainSupportsThrowingFromDestructors) {
    +  class ThrowsInDestructor {
    +   public:
    +    ~ThrowsInDestructor() noexcept(false) { throw std::exception(); }
    +  };
    +
    +  try {
    +    { ThrowsInDestructor obj; }
    +    FAIL() << "Should have thrown";
    +  } catch (const std::exception& e) {
    +    SUCCEED() << "Caught exception";
    +  }
    +}
    +#endif  // __cpp_exceptions
    +
    +TEST_F(EnvTest, GetStringUtfLength) {
    +  Local str = env_.NewStringUtf("Foo");
    +  size_t len = env_.GetStringUtfLength(str);
    +  EXPECT_EQ(3, len);
    +}
    +
    +TEST_F(EnvTest, GetStringUtfRegion) {
    +  Local str = env_.NewStringUtf("Foo");
    +  std::string result = env_.GetStringUtfRegion(str, 1, 2);
    +  EXPECT_EQ("oo", result);
    +}
    +
    +TEST_F(EnvTest, ToString) {
    +  Local str = env_.NewStringUtf("Foo");
    +  std::string result = str.ToString(env_);
    +  EXPECT_EQ("Foo", result);
    +}
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    
    From 3ac9e71b5b873db156dc8efdb8ab4536e5346d99 Mon Sep 17 00:00:00 2001
    From: mcg 
    Date: Fri, 17 Jul 2020 12:49:42 -0700
    Subject: [PATCH 040/109] Rework ToJni conversions to enable pass-through of
     JNI types
    
    Previously, all types needed to be in the JniTypeMap, but this was never
    intended to be the end state because it would require an entry for all subtypes
    of Object which couldn't scale.
    
    The new implementation based on ranked choice overload selection also resolves
    ambiguities. For example: unsigned char could be a uint8_t (which converts to
    jbyte) or the underlying type for jboolean. Absent the ranking, an argument of
    type unsigned char would resolve to multiple overloads and would be ambiguous.
    
    PiperOrigin-RevId: 321835773
    ---
     firestore/src/jni/traits.h             | 98 +++++++++++++++++++++-----
     firestore/src/tests/jni/traits_test.cc | 62 ++++++++++++++++
     2 files changed, 143 insertions(+), 17 deletions(-)
    
    diff --git a/firestore/src/jni/traits.h b/firestore/src/jni/traits.h
    index 99992ed653..777981cfc1 100644
    --- a/firestore/src/jni/traits.h
    +++ b/firestore/src/jni/traits.h
    @@ -3,6 +3,8 @@
     
     #include 
     
    +#include 
    +
     #include "app/src/include/firebase/internal/type_traits.h"
     #include "firestore/src/jni/jni_fwd.h"
     
    @@ -36,7 +38,8 @@ template <> struct IsReference : public true_type {};
     // MARK: Type mapping
     
     // A compile-time map from C++ types to their JNI equivalents.
    -template  struct JniTypeMap {};
    +template  struct JniTypeMap { using type = jobject; };
    +
     template <> struct JniTypeMap { using type = jboolean; };
     template <> struct JniTypeMap { using type = jbyte; };
     template <> struct JniTypeMap { using type = jchar; };
    @@ -47,12 +50,16 @@ template <> struct JniTypeMap { using type = jfloat; };
     template <> struct JniTypeMap { using type = jdouble; };
     template <> struct JniTypeMap { using type = jsize; };
     
    -template <> struct JniTypeMap { using type = jobject; };
    -template <> struct JniTypeMap { using type = jstring; };
    -
     template <> struct JniTypeMap { using type = jobject; };
     template <> struct JniTypeMap { using type = jstring; };
     
    +template  struct JniTypeMap> {
    +  using type = typename JniTypeMap::type;
    +};
    +template  struct JniTypeMap> {
    +  using type = typename JniTypeMap::type;
    +};
    +
     template 
     using JniType = typename JniTypeMap>::type;
     
    @@ -70,26 +77,83 @@ using EnableForReference =
     
     // MARK: Type converters
     
    -// Converts C++ primitives to their equivalent JNI primitive types by casting.
    -template 
    -EnableForPrimitive> ToJni(const T& value) {
    +namespace internal {
    +
    +/**
    + * An explicit ordering for overload resolution of JNI conversions. This allows
    + * SFINAE without needing to make all the enable_if cases mutually exclusive.
    + *
    + * When finding a JNI converter, we try the following, in order:
    + *   * pass through, for JNI primitive types;
    + *   * static casts, for C++ primitive types;
    + *   * pass through, for JNI reference types like jobject;
    + *   * unwrapping, for JNI reference wrapper types like `Object` or
    + *     `Local`.
    + *
    + * `ConverterChoice` is a recursive type, defined such that `ConverterChoice<0>`
    + * is the most derived type, `ConverterChoice<1>` and beyond are progressively
    + * less derived. This causes the compiler to prioritize the overloads with
    + * lower-numbered `ConverterChoice`s first, allowing compilation to succeed even
    + * if multiple unqualified overloads would match, and would otherwise fail due
    + * to an ambiguity.
    + */
    +template 
    +struct ConverterChoice : public ConverterChoice {};
    +
    +template <>
    +struct ConverterChoice<3> {};
    +
    +/**
    + * Converts JNI primitive types to themselves.
    + */
    +template ::value>::type>
    +T RankedToJni(T value, ConverterChoice<0>) {
    +  return value;
    +}
    +
    +/**
    + * Converts C++ primitive types to their equivalent JNI primitive types by
    + * casting.
    + */
    +template >::value>::type>
    +JniType RankedToJni(T value, ConverterChoice<1>) {
       return static_cast>(value);
     }
     
    -// Converts JNI wrapper reference types (like `const Object&`) and any ownership
    -// wrappers of those types to their underlying `jobject`-derived reference.
    -template 
    -EnableForReference> ToJni(const T& value) {
    -  return value.get();
    +/**
    + * Converts direct use of a JNI reference types to themselves.
    + */
    +template ::value>::type>
    +T RankedToJni(T value, ConverterChoice<2>) {
    +  return value;
     }
    -template 
    -J ToJni(const T& value) {
    +
    +#if defined(_STLPORT_VERSION)
    +using nullptr_t = decltype(nullptr);
    +#else
    +using nullptr_t = std::nullptr_t;
    +#endif
    +
    +inline jobject RankedToJni(nullptr_t, ConverterChoice<2>) { return nullptr; }
    +
    +/**
    + * Converts wrapper types to JNI references by unwrapping them.
    + */
    +template >
    +J RankedToJni(const T& value, ConverterChoice<3>) {
       return value.get();
     }
     
    -// Preexisting JNI types can be passed directly. This makes incremental
    -// migration possible. Ideally this could eventually be removed.
    -inline jobject ToJni(jobject value) { return value; }
    +}  // namespace internal
    +
    +template 
    +auto ToJni(const T& value)
    +    -> decltype(internal::RankedToJni(value, internal::ConverterChoice<0>{})) {
    +  return internal::RankedToJni(value, internal::ConverterChoice<0>{});
    +}
     
     }  // namespace jni
     }  // namespace firestore
    diff --git a/firestore/src/tests/jni/traits_test.cc b/firestore/src/tests/jni/traits_test.cc
    index 6bb2032df1..4e0094998a 100644
    --- a/firestore/src/tests/jni/traits_test.cc
    +++ b/firestore/src/tests/jni/traits_test.cc
    @@ -4,13 +4,17 @@
     
     #include 
     
    +#include "firestore/src/jni/env.h"
     #include "firestore/src/jni/object.h"
    +#include "firestore/src/jni/ownership.h"
    +#include "firestore/src/jni/string.h"
     #include "firestore/src/tests/firestore_integration_test.h"
     #include "gtest/gtest.h"
     
     namespace firebase {
     namespace firestore {
     namespace jni {
    +namespace {
     
     using testing::StaticAssertTypeEq;
     
    @@ -28,6 +32,11 @@ void ExpectConvertsPrimitive() {
       EXPECT_EQ(jni_value, static_cast(cpp_value));
     }
     
    +class TestObject : public Object {
    + public:
    +  using Object::Object;
    +};
    +
     TEST_F(TraitsTest, ConvertsPrimitives) {
       ExpectConvertsPrimitive();
       ExpectConvertsPrimitive();
    @@ -40,6 +49,18 @@ TEST_F(TraitsTest, ConvertsPrimitives) {
       ExpectConvertsPrimitive();
     }
     
    +TEST_F(TraitsTest, PassesThroughJniPrimitives) {
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +  ExpectConvertsPrimitive();
    +}
    +
     TEST_F(TraitsTest, ConvertsObjects) {
       Object cpp_value;
       jobject jni_value = ToJni(cpp_value);
    @@ -53,6 +74,46 @@ TEST_F(TraitsTest, ConvertsObjects) {
       EXPECT_EQ(jni_value, nullptr);
     }
     
    +TEST_F(TraitsTest, ConvertsStrings) {
    +  Env env;
    +
    +  String empty_value;
    +  jstring jni_value = ToJni(empty_value);
    +  EXPECT_EQ(jni_value, nullptr);
    +
    +  Local cpp_value = env.NewStringUtf("testing");
    +  jni_value = ToJni(cpp_value);
    +  EXPECT_EQ(jni_value, cpp_value.get());
    +
    +  jstring jstring_value = nullptr;
    +  jni_value = ToJni(jstring_value);
    +  EXPECT_EQ(jni_value, nullptr);
    +}
    +
    +TEST_F(TraitsTest, ConvertsArbitrarySubclassesOfObject) {
    +  TestObject cpp_value;
    +  jobject jni_value = ToJni(cpp_value);
    +  EXPECT_EQ(jni_value, nullptr);
    +}
    +
    +TEST_F(TraitsTest, ConvertsOwnershipWrappers) {
    +  StaticAssertTypeEq>, jobject>();
    +  StaticAssertTypeEq>, jstring>();
    +  StaticAssertTypeEq&>, jstring>();
    +
    +  Local local_value;
    +  jobject jni_value = ToJni(local_value);
    +  EXPECT_EQ(jni_value, nullptr);
    +
    +  Local test_value;
    +  jni_value = ToJni(test_value);
    +  EXPECT_EQ(jni_value, nullptr);
    +
    +  Global global_value;
    +  jni_value = ToJni(global_value);
    +  EXPECT_EQ(jni_value, nullptr);
    +}
    +
     // Conversion implicitly tests type mapping. Additionally test variations of
     // types that should be equivalent.
     TEST_F(TraitsTest, DecaysBeforeMappingTypes) {
    @@ -68,6 +129,7 @@ TEST_F(TraitsTest, DecaysBeforeMappingTypes) {
       StaticAssertTypeEq, jobject>();
     }
     
    +}  // namespace
     }  // namespace jni
     }  // namespace firestore
     }  // namespace firebase
    
    From 667478f4abd6a9b61645f1916bc880c05e99c456 Mon Sep 17 00:00:00 2001
    From: mcg 
    Date: Tue, 21 Jul 2020 11:05:52 -0700
    Subject: [PATCH 041/109] Add support for calling methods and getting fields.
    
    PiperOrigin-RevId: 322398533
    ---
     firestore/src/jni/call_traits.h     | 115 ++++++++++++++++++
     firestore/src/jni/class.h           |  30 +++++
     firestore/src/jni/env.cc            |  33 ++++++
     firestore/src/jni/env.h             | 173 ++++++++++++++++++++++++++++
     firestore/src/jni/jni_fwd.h         |   1 +
     firestore/src/jni/traits.h          |   3 +
     firestore/src/tests/jni/env_test.cc |  87 ++++++++++++++
     7 files changed, 442 insertions(+)
     create mode 100644 firestore/src/jni/call_traits.h
     create mode 100644 firestore/src/jni/class.h
    
    diff --git a/firestore/src/jni/call_traits.h b/firestore/src/jni/call_traits.h
    new file mode 100644
    index 0000000000..d0e0d8814d
    --- /dev/null
    +++ b/firestore/src/jni/call_traits.h
    @@ -0,0 +1,115 @@
    +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CALL_TRAITS_H_
    +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CALL_TRAITS_H_
    +
    +#include 
    +
    +#include "firestore/src/jni/traits.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +/**
    + * Traits describing how to invoke JNI methods uniformly for various JNI return
    + * types.
    + *
    + * By default, uses Object variants (e.g. `CallObjectMethod`), since most types
    + * will use this form. Only primitives need special forms.
    + */
    +template 
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallObjectMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticObjectField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticObjectMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallBooleanMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticBooleanField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticBooleanMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallByteMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticByteField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticByteMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallCharMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticCharField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticCharMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallShortMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticShortField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticShortMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallIntMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticIntField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticIntMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallLongMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticLongField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticLongMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallFloatMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticFloatField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticFloatMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallDoubleMethod;
    +  static constexpr auto kGetStaticField = &JNIEnv::GetStaticDoubleField;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticDoubleMethod;
    +};
    +
    +template <>
    +struct CallTraits {
    +  static constexpr auto kCall = &JNIEnv::CallVoidMethod;
    +  static constexpr auto kCallStatic = &JNIEnv::CallStaticVoidMethod;
    +};
    +
    +/**
    + * The type of the result of a JNI function. For reference types, it's always
    + * a `Local` wrapper of the type. For primitive types, it's just the type
    + * itself.
    + */
    +template >::value>
    +struct ResultTypeMap {
    +  using type = T;
    +};
    +
    +template 
    +struct ResultTypeMap {
    +  using type = Local;
    +};
    +
    +template <>
    +struct ResultTypeMap {
    +  using type = void;
    +};
    +
    +template 
    +using ResultType = typename ResultTypeMap::type;
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    +
    +#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CALL_TRAITS_H_
    diff --git a/firestore/src/jni/class.h b/firestore/src/jni/class.h
    new file mode 100644
    index 0000000000..e85edc86d9
    --- /dev/null
    +++ b/firestore/src/jni/class.h
    @@ -0,0 +1,30 @@
    +#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CLASS_H_
    +#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CLASS_H_
    +
    +#include "firestore/src/jni/object.h"
    +
    +namespace firebase {
    +namespace firestore {
    +namespace jni {
    +
    +/**
    + * A wrapper for a JNI `jclass` that adds additional behavior. This is a proxy
    + * for a Java Class in the JVM.
    + *
    + * `Class` merely holds values with `jclass` type, see `Local` and `Global`
    + * template subclasses for reference-type-aware wrappers that automatically
    + * manage the lifetime of JNI objects.
    + */
    +class Class : public Object {
    + public:
    +  Class() = default;
    +  explicit Class(jclass clazz) : Object(clazz) {}
    +
    +  jclass get() const override { return static_cast(object_); }
    +};
    +
    +}  // namespace jni
    +}  // namespace firestore
    +}  // namespace firebase
    +
    +#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CLASS_H_
    diff --git a/firestore/src/jni/env.cc b/firestore/src/jni/env.cc
    index 62bf9177f5..32c265bb68 100644
    --- a/firestore/src/jni/env.cc
    +++ b/firestore/src/jni/env.cc
    @@ -4,6 +4,39 @@ namespace firebase {
     namespace firestore {
     namespace jni {
     
    +Local Env::FindClass(const char* name) {
    +  jclass result = env_->FindClass(name);
    +  RecordException();
    +  return Local(env_, result);
    +}
    +
    +jmethodID Env::GetMethodId(const Class& clazz, const char* name,
    +                           const char* sig) {
    +  if (!ok()) return nullptr;
    +
    +  jmethodID result = env_->GetMethodID(clazz.get(), name, sig);
    +  RecordException();
    +  return result;
    +}
    +
    +jfieldID Env::GetStaticFieldId(const Class& clazz, const char* name,
    +                               const char* sig) {
    +  if (!ok()) return nullptr;
    +
    +  jfieldID result = env_->GetStaticFieldID(ToJni(clazz), name, sig);
    +  RecordException();
    +  return result;
    +}
    +
    +jmethodID Env::GetStaticMethodId(const Class& clazz, const char* name,
    +                                 const char* sig) {
    +  if (!ok()) return nullptr;
    +
    +  jmethodID result = env_->GetStaticMethodID(ToJni(clazz), name, sig);
    +  RecordException();
    +  return result;
    +}
    +
     Local Env::NewStringUtf(const char* bytes) {
       if (!ok()) return {};
     
    diff --git a/firestore/src/jni/env.h b/firestore/src/jni/env.h
    index c5020a8533..a77d64e9cc 100644
    --- a/firestore/src/jni/env.h
    +++ b/firestore/src/jni/env.h
    @@ -5,6 +5,9 @@
     
     #include 
     
    +#include "app/meta/move.h"
    +#include "firestore/src/jni/call_traits.h"
    +#include "firestore/src/jni/class.h"
     #include "firestore/src/jni/object.h"
     #include "firestore/src/jni/ownership.h"
     #include "firestore/src/jni/string.h"
    @@ -14,6 +17,9 @@ namespace firebase {
     namespace firestore {
     namespace jni {
     
    +// Since we're targeting STLPort, `std::invoke` is not available.
    +#define INVOKE(env, method, ...) ((env)->*(method))(__VA_ARGS__);
    +
     /**
      * A wrapper around a JNIEnv pointer that makes dealing with JNI simpler in C++,
      * by:
    @@ -42,6 +48,119 @@ class Env {
       /** Returns the underlying JNIEnv pointer. */
       JNIEnv* get() const { return env_; }
     
    +  // MARK: Class Operations
    +
    +  /**
    +   * Finds the java class associated with the given name which should be
    +   * formatted like "java/lang/Object".
    +   */
    +  Local FindClass(const char* name);
    +
    +  // MARK: Object Operations
    +
    +  /**
    +   * Creates a new Java object of the given class, returning the result in a
    +   * reference wrapper of type T.
    +   *
    +   * @tparam T The C++ type to which the method should be coerced.
    +   * @tparam Args The C++ types of the arguments to the method.
    +   * @param clazz The Java class of the resulting object.
    +   * @param method The constructor method to invoke.
    +   * @param args The C++ arguments of the constructor. These will be converted
    +   *     to their JNI equivalent value with a call to ToJni before invocation.
    +   * @return a local reference to the newly-created object.
    +   */
    +  template 
    +  Local New(const Class& clazz, jmethodID method, Args&&... args) {
    +    if (!ok()) return {};
    +
    +    jobject result =
    +        env_->NewObject(clazz.get(), method, ToJni(Forward(args))...);
    +    RecordException();
    +    return MakeResult(result);
    +  }
    +
    +  // MARK: Calling Instance Methods
    +
    +  /**
    +   * Finds the method on the given class that's associated with the method name
    +   * and signature.
    +   */
    +  jmethodID GetMethodId(const Class& clazz, const char* name, const char* sig);
    +
    +  /**
    +   * Invokes the JNI instance method using the `Call*Method` appropriate to the
    +   * return type T.
    +   *
    +   * @tparam T The C++ return type to which the method should be coerced.
    +   * @param object The object to use as `this` for the invocation.
    +   * @param method The method to invoke.
    +   * @param args The C++ arguments of the method. These will be converted to
    +   *     their JNI equivalent value with a call to ToJni before invocation.
    +   * @return The primitive result if T is a primitive, nothing if T is `void`,
    +   *     or a local reference to the returned object.
    +   */
    +  template 
    +  ResultType Call(const Object& object, jmethodID method, Args&&... args) {
    +    auto env_method = CallTraits>::kCall;
    +    return CallHelper(env_method, object.get(), method,
    +                         ToJni(Forward(args))...);
    +  }
    +
    +  // MARK: Accessing Static Fields
    +
    +  jfieldID GetStaticFieldId(const Class& clazz, const char* name,
    +                            const char* sig);
    +
    +  /**
    +   * Returns the value of the given field using the GetStatic*Field method
    +   * appropriate to the field type T.
    +   *
    +   * @tparam T The C++ type to which the field value should be coerced.
    +   * @param clazz The class to use as the container for the field.
    +   * @param field The field to get.
    +   * @return a local reference to the field value.
    +   */
    +  template 
    +  ResultType GetStaticField(const Class& clazz, jfieldID field) {
    +    if (!ok()) return {};
    +
    +    auto env_method = CallTraits>::kGetStaticField;
    +    auto result = INVOKE(env_, env_method, clazz.get(), field);
    +    RecordException();
    +    return MakeResult(result);
    +  }
    +
    +  // MARK: Calling Static Methods
    +
    +  /**
    +   * Finds the method on the given class that's associated with the method name
    +   * and signature.
    +   */
    +  jmethodID GetStaticMethodId(const Class& clazz, const char* name,
    +                              const char* sig);
    +
    +  /**
    +   * Invokes the JNI static method using the CallStatic*Method appropriate to
    +   * the return type T.
    +   *
    +   * @tparam T The C++ return type to which the method should be coerced.
    +   * @tparam Args The C++ types of the arguments to the method.
    +   * @param clazz The class against which to invoke the static method.
    +   * @param method The method to invoke.
    +   * @param args The C++ arguments of the method. These will be converted to
    +   *     their JNI equivalent value with a call to ToJni before invocation.
    +   * @return The primitive result if T is a primitive, nothing if T is `void`,
    +   *     or a local reference to the returned object.
    +   */
    +  template 
    +  ResultType CallStatic(const Class& clazz, jmethodID method,
    +                           Args&&... args) {
    +    auto env_method = CallTraits>::kCallStatic;
    +    return CallHelper(env_method, clazz.get(), method,
    +                         ToJni(Forward(args))...);
    +  }
    +
       // MARK: String Operations
     
       /**
    @@ -74,12 +193,66 @@ class Env {
       }
     
      private:
    +  /**
    +   * Invokes the JNI instance method using the given method reference on JNIEnv.
    +   *
    +   * @tparam T The non-void C++ return type to which the method's result should
    +   *     be coerced.
    +   * @param env_method A method reference from JNIEnv, appropriate for the
    +   *     return type T, and the kind of method being invoked (instance or
    +   *     static). Use `CallTraits>::kCall` or `kCallStatic` to find
    +   *     the right method.
    +   * @param args The method and JNI arguments of the JNI method, including the
    +   *     class or object, jmethodID, and any arguments to pass.
    +   * @return The primitive result if T is a primitive or a local reference to
    +   *     the returned object.
    +   */
    +  template 
    +  typename enable_if::value, ResultType>::type CallHelper(
    +      M&& env_method, Args&&... args) {
    +    if (!ok()) return {};
    +
    +    auto result = INVOKE(env_, env_method, Forward(args)...);
    +    RecordException();
    +    return MakeResult(result);
    +  }
    +
    +  /**
    +   * Invokes a JNI call method if the return type is `void`.
    +   *
    +   * If `T` is anything but `void`, the overload is disabled.
    +   */
    +  template 
    +  typename enable_if::value, void>::type CallHelper(
    +      M&& env_method, Args&&... args) {
    +    if (!ok()) return;
    +
    +    INVOKE(env_, env_method, Forward(args)...);
    +    RecordException();
    +  }
    +
       void RecordException();
     
    +  template 
    +  EnableForPrimitive MakeResult(JniType value) {
    +    return static_cast(value);
    +  }
    +
    +  template 
    +  EnableForReference> MakeResult(jobject object) {
    +    // JNI object method results are always jobject, even when the actual type
    +    // is jstring or jclass. Cast to the correct type here so that Local
    +    // doesn't have to account for this.
    +    auto typed_object = static_cast>(object);
    +    return Local(env_, typed_object);
    +  }
    +
       JNIEnv* env_ = nullptr;
       jthrowable last_exception_ = nullptr;
     };
     
    +#undef INVOKE
    +
     }  // namespace jni
     }  // namespace firestore
     }  // namespace firebase
    diff --git a/firestore/src/jni/jni_fwd.h b/firestore/src/jni/jni_fwd.h
    index 9ca32b09ce..ee105200f3 100644
    --- a/firestore/src/jni/jni_fwd.h
    +++ b/firestore/src/jni/jni_fwd.h
    @@ -20,6 +20,7 @@ class Global;
     template 
     class NonOwning;
     
    +class Class;
     class Object;
     class String;
     
    diff --git a/firestore/src/jni/traits.h b/firestore/src/jni/traits.h
    index 777981cfc1..135de40cf3 100644
    --- a/firestore/src/jni/traits.h
    +++ b/firestore/src/jni/traits.h
    @@ -48,8 +48,11 @@ template <> struct JniTypeMap { using type = jint; };
     template <> struct JniTypeMap { using type = jlong; };
     template <> struct JniTypeMap { using type = jfloat; };
     template <> struct JniTypeMap { using type = jdouble; };
    +
     template <> struct JniTypeMap { using type = jsize; };
    +template <> struct JniTypeMap { using type = void; };
     
    +template <> struct JniTypeMap { using type = jclass; };
     template <> struct JniTypeMap { using type = jobject; };
     template <> struct JniTypeMap { using type = jstring; };
     
    diff --git a/firestore/src/tests/jni/env_test.cc b/firestore/src/tests/jni/env_test.cc
    index eeb76311f7..7659049bdd 100644
    --- a/firestore/src/tests/jni/env_test.cc
    +++ b/firestore/src/tests/jni/env_test.cc
    @@ -31,6 +31,93 @@ TEST_F(EnvTest, ToolchainSupportsThrowingFromDestructors) {
     }
     #endif  // __cpp_exceptions
     
    +TEST_F(EnvTest, ConstructsObjects) {
    +  Local clazz = env_.FindClass("java/lang/Integer");
    +  jmethodID new_integer = env_.GetMethodId(clazz, "", "(I)V");
    +
    +  Local result = env_.New(clazz, new_integer, 42);
    +  EXPECT_EQ("42", result.ToString(env_));
    +}
    +
    +TEST_F(EnvTest, CallsBooleanMethods) {
    +  Local haystack = env_.NewStringUtf("Food");
    +  Local needle = env_.NewStringUtf("Foo");
    +
    +  Local clazz = env_.FindClass("java/lang/String");
    +  jmethodID starts_with =
    +      env_.GetMethodId(clazz, "startsWith", "(Ljava/lang/String;)Z");
    +
    +  bool result = env_.Call(haystack, starts_with, needle);
    +  EXPECT_TRUE(result);
    +
    +  needle = env_.NewStringUtf("Bar");
    +  result = env_.Call(haystack, starts_with, needle);
    +  EXPECT_FALSE(result);
    +}
    +
    +TEST_F(EnvTest, CallsIntMethods) {
    +  Local str = env_.NewStringUtf("Foo");
    +
    +  Local clazz = env_.FindClass("java/lang/String");
    +  jmethodID index_of = env_.GetMethodId(clazz, "indexOf", "(I)I");
    +
    +  int32_t result = env_.Call(str, index_of, jint('o'));
    +  EXPECT_EQ(1, result);
    +
    +  result = env_.Call(str, index_of, jint('z'));
    +  EXPECT_EQ(-1, result);
    +}
    +
    +TEST_F(EnvTest, CallsObjectMethods) {
    +  Local str = env_.NewStringUtf("Foo");
    +
    +  Local clazz = env_.FindClass("java/lang/String");
    +  jmethodID to_lower_case =
    +      env_.GetMethodId(clazz, "toLowerCase", "()Ljava/lang/String;");
    +
    +  Local result = env_.Call(str, to_lower_case);
    +  EXPECT_EQ("foo", result.ToString(env_));
    +}
    +
    +TEST_F(EnvTest, CallsVoidMethods) {
    +  Local clazz = env_.FindClass("java/lang/StringBuilder");
    +  jmethodID ctor = env_.GetMethodId(clazz, "", "()V");
    +  jmethodID get_length = env_.GetMethodId(clazz, "length", "()I");
    +  jmethodID set_length = env_.GetMethodId(clazz, "setLength", "(I)V");
    +
    +  Local builder = env_.New(clazz, ctor);
    +  env_.Call(builder, set_length, 42);
    +
    +  int32_t length = env_.Call(builder, get_length);
    +  EXPECT_EQ(length, 42);
    +}
    +
    +TEST_F(EnvTest, GetsStaticFields) {
    +  Local clazz = env_.FindClass("java/lang/String");
    +  jfieldID comparator = env_.GetStaticFieldId(clazz, "CASE_INSENSITIVE_ORDER",
    +                                              "Ljava/util/Comparator;");
    +
    +  Local result = env_.GetStaticField(clazz, comparator);
    +  EXPECT_NE(result.get(), nullptr);
    +}
    +
    +TEST_F(EnvTest, CallsStaticObjectMethods) {
    +  Local clazz = env_.FindClass("java/lang/String");
    +  jmethodID value_of_int =
    +      env_.GetStaticMethodId(clazz, "valueOf", "(I)Ljava/lang/String;");
    +
    +  Local result = env_.CallStatic(clazz, value_of_int, 42);
    +  EXPECT_EQ("42", result.ToString(env_));
    +}
    +
    +TEST_F(EnvTest, CallsStaticVoidMethods) {
    +  Local clazz = env_.FindClass("java/lang/System");
    +  jmethodID gc = env_.GetStaticMethodId(clazz, "gc", "()V");
    +
    +  env_.CallStatic(clazz, gc);
    +  EXPECT_TRUE(env_.ok());
    +}
    +
     TEST_F(EnvTest, GetStringUtfLength) {
       Local str = env_.NewStringUtf("Foo");
       size_t len = env_.GetStringUtfLength(str);
    
    From d464bcfd26eec7735d7a8374b0cd325e77fc0280 Mon Sep 17 00:00:00 2001
    From: anonymous-akorn <66133366+anonymous-akorn@users.noreply.github.com>
    Date: Mon, 3 Aug 2020 12:47:47 -0700
    Subject: [PATCH 042/109] Create dummy workflow for integration tests (#106)
    
    To manually trigger a workflow in a branch, a workflow with the same name needs to exist in master. This adds such a workflow.
    ---
     .github/workflows/integration_tests.yml | 16 ++++++++++++++++
     1 file changed, 16 insertions(+)
     create mode 100644 .github/workflows/integration_tests.yml
    
    diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
    new file mode 100644
    index 0000000000..be9f08b114
    --- /dev/null
    +++ b/.github/workflows/integration_tests.yml
    @@ -0,0 +1,16 @@
    +# Dummy workflow file so that the corresponding workflow
    +# can be manually triggered in branches other than master.
    +name: Integration tests
    +
    +on:
    +  workflow_dispatch:
    +    inputs:
    +      commitId:
    +        description: 'description'
    +
    +jobs:
    +  build:
    +    runs-on: ubuntu-latest
    +    steps:
    +      - name: Dummy step
    +        run: echo "Hello, World!"
    
    From 95beec9cacc2814d1ef7954b368cbc6dbf64301d Mon Sep 17 00:00:00 2001
    From: chkuang-g <31869252+chkuang-g@users.noreply.github.com>
    Date: Tue, 8 Sep 2020 15:53:29 -0700
    Subject: [PATCH 043/109] Create feature-request.md
    
    ---
     .github/ISSUE_TEMPLATE/feature-request.md | 26 +++++++++++++++++++++++
     1 file changed, 26 insertions(+)
     create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md
    
    diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
    new file mode 100644
    index 0000000000..797f1354c4
    --- /dev/null
    +++ b/.github/ISSUE_TEMPLATE/feature-request.md
    @@ -0,0 +1,26 @@
    +---
    +name: ➕ Feature request
    +about: If you have a feature request for the Firebase C++ SDK, file it here.
    +labels: 'type: feature request'
    +---
    +
    +### [READ] Guidelines
    +
    +When filing a feature request please make sure the issue title starts with "FR:".
    +
    +A good feature request ideally
    +* is either immediately obvious (i.e. "Add Sign in with Apple support"), or
    +* starts with a use case that is not achievable with the existing Firebase API and
    +  includes an API proposal that would make the use case possible. The proposed API
    +  change does not need to be very specific.
    +
    +Once you've read this section, please delete it and fill out the rest of the template.
    +
    +### Feature proposal
    +
    +* Firebase Component: _____ (Auth, Core, Database, Firestore, Messaging, Storage, etc)
    +
    +Describe your use case and/or feature request here.
    
    From cea22cfdd2971abe7a9bddb0f8cc9aed169598a1 Mon Sep 17 00:00:00 2001
    From: chkuang-g <31869252+chkuang-g@users.noreply.github.com>
    Date: Tue, 8 Sep 2020 16:03:14 -0700
    Subject: [PATCH 044/109] Update and rename firebase-cpp-sdk-issue.md to
     issue.md
    
    Update issue template
    ---
     .../ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md  | 24 ------------
     .github/ISSUE_TEMPLATE/issue.md               | 39 +++++++++++++++++++
     2 files changed, 39 insertions(+), 24 deletions(-)
     delete mode 100644 .github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md
     create mode 100644 .github/ISSUE_TEMPLATE/issue.md
    
    diff --git a/.github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md b/.github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md
    deleted file mode 100644
    index 916a4ce47f..0000000000
    --- a/.github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md
    +++ /dev/null
    @@ -1,24 +0,0 @@
    ----
    -name: Firebase C++ SDK issue
    -about: Please use this template to report issues with the Firebase C++ SDK.
    -title: ''
    -labels: new
    -assignees: ''
    -
    ----
    -
    -### Please fill in the following fields:
    -Pre-built SDK from the [website](https://firebase.google.com/download/cpp) or open-source from this repo:
    -Firebase C++ SDK version: 
    -Firebase plugins in use (Auth, Database, etc.):
    -Additional SDKs you are using (Facebook, AdMob, etc.): 
    -Platform you are using the C++ SDK on (Mac, Windows, or Linux): 
    -Platform you are targeting (iOS, Android, and/or desktop): 
    -
    -### Please describe the issue here:
    -(Please list the full steps to reproduce the issue. Include device logs, Unity logs, and stack traces if available.)
    -
    -### Please answer the following, if applicable:
    -Have you been able to reproduce this issue with just the [Firebase C++ quickstarts](https://github.com/firebase/quickstart-cpp) ?
    -
    -What's the issue repro rate? (eg 100%, 1/5 etc)
    diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md
    new file mode 100644
    index 0000000000..7dfa8f4233
    --- /dev/null
    +++ b/.github/ISSUE_TEMPLATE/issue.md
    @@ -0,0 +1,39 @@
    +---
    +name: 🐞 Bug report
    +about: Please use this template to report bugs with the Firebase C++ SDK.
    +labels: new
    +---
    +
    +
    +### [REQUIRED] Please fill in the following fields:
    +
    +  * Pre-built SDK from the [website](https://firebase.google.com/download/cpp) or open-source from this repo: _____
    +  * Firebase C++ SDK version: _____
    +  * Problematic Firebase Component (Auth, Database, etc.): _____
    +  * Other Firebase Components in use (Auth, Database, etc.): _____
    +  * Platform you are using the C++ SDK on (Mac, Windows, or Linux): _____
    +  * Platform you are targeting (iOS, Android, and/or desktop): _____
    +
    +### [REQUIRED] Please describe the issue here:
    +
    +(Please list the full steps to reproduce the issue. Include device logs, Unity logs, and stack traces if available.)
    +
    +#### Steps to reproduce:
    +
    +Have you been able to reproduce this issue with just the [Firebase C++ quickstarts](https://github.com/firebase/quickstart-cpp) ?
    +What's the issue repro rate? (eg 100%, 1/5 etc)
    +
    +What happened? How can we make the problem occur?
    +This could be a description, log/console output, etc.
    +
    +If you have a downloadable sample project that reproduces the bug you're reporting, you will
    +likely receive a faster response on your issue.
    +
    +#### Relevant Code:
    +
    +```
    +// TODO(you): code here to reproduce the problem
    +```
    
    From b8f1e9b7783b278fc3499f894b479d7b3ab23171 Mon Sep 17 00:00:00 2001
    From: chkuang-g <31869252+chkuang-g@users.noreply.github.com>
    Date: Tue, 8 Sep 2020 17:48:22 -0700
    Subject: [PATCH 045/109] Update issue.md
    
    ---
     .github/ISSUE_TEMPLATE/issue.md | 9 +++++----
     1 file changed, 5 insertions(+), 4 deletions(-)
    
    diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md
    index 7dfa8f4233..69be17178d 100644
    --- a/.github/ISSUE_TEMPLATE/issue.md
    +++ b/.github/ISSUE_TEMPLATE/issue.md
    @@ -3,6 +3,7 @@ name: 🐞 Bug report
     about: Please use this template to report bugs with the Firebase C++ SDK.
     labels: new
     ---
    +
     
    +
    +### [READ] For Firebase Unity SDK question, please report to [Firebase Unity Sample](https://github.com/firebase/quickstart-unity/issues/new/choose)
    +
    +Once you've read this section and determined that your issue is appropriate for this repository, please delete this section.
    +
    +### [REQUIRED] Please fill in the following fields:
    +
    +  * Pre-built SDK from the [website](https://firebase.google.com/download/cpp) or open-source from this repo: _____
    +  * Firebase C++ SDK version: _____
    +  * Main Firebase Components in concern: _____ (Auth, Database, etc.)
    +  * Other Firebase Components in use: _____ (Auth, Database, etc.)
    +  * Platform you are using the C++ SDK on: _____ (Mac, Windows, or Linux)
    +  * Platform you are targeting: _____ (iOS, Android, and/or desktop)
    +
    +### [REQUIRED] Please describe the question here:
    diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
    index 797f1354c4..1c72c696eb 100644
    --- a/.github/ISSUE_TEMPLATE/feature-request.md
    +++ b/.github/ISSUE_TEMPLATE/feature-request.md
    @@ -1,8 +1,12 @@
     ---
    -name: ➕ Feature request
    +name: "➕ Feature request"
     about: If you have a feature request for the Firebase C++ SDK, file it here.
    +title: ''
     labels: 'type: feature request'
    +assignees: ''
    +
     ---
    +