diff --git a/.github/workflows/package-cpp-sdk b/.github/workflows/package-cpp-sdk new file mode 100644 index 0000000000..4425d7cb67 --- /dev/null +++ b/.github/workflows/package-cpp-sdk @@ -0,0 +1,29 @@ +name: Package C++ SDK' + +on: + workflow_dispatch: + inputs: + commitId: + description: 'commit ID to package' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Fetch binutils source + uses: boyeborg/fetch-url-action@v1.1 + with: + url: https://ftpmirror.gnu.org/binutils/binutils-2.34.tar.xz + - name: untar binutils source + run: tar -xvzf binutils-2.34.tar.xz + - name: configure binutils + run: ./configure --enable-targets=all --prefix=/tmp/bin + working-directory: ./binutils-2.34 + - name: make binutils + run: make + working-directory: ./binutils-2.34 + - name: install binutils in /tmp/bin + run: make install + working-directory: ./binutils-2.34 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/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); 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/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) { 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/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()); 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/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' 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()