diff --git a/messaging/integration_test/CMakeLists.txt b/messaging/integration_test/CMakeLists.txt index 3e33aa9bea..8f069b2b9b 100644 --- a/messaging/integration_test/CMakeLists.txt +++ b/messaging/integration_test/CMakeLists.txt @@ -237,7 +237,7 @@ endif() # Add the Firebase libraries to the target using the function from the SDK. add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL) # Note that firebase_app needs to be last in the list. -set(firebase_libs firebase_messaging firebase_app) +set(firebase_libs firebase_functions firebase_messaging firebase_app) set(gtest_libs gtest gmock) target_link_libraries(${integration_test_target_name} ${firebase_libs} ${gtest_libs} ${ADDITIONAL_LIBS}) diff --git a/messaging/integration_test/Podfile b/messaging/integration_test/Podfile index 507c22d199..dd70df720b 100644 --- a/messaging/integration_test/Podfile +++ b/messaging/integration_test/Podfile @@ -5,11 +5,13 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' pod 'Firebase/Messaging', '10.25.0' + pod 'Firebase/Functions', '10.25.0' end target 'integration_test_tvos' do platform :tvos, '12.0' pod 'Firebase/Messaging', '10.25.0' + pod 'Firebase/Functions', '10.25.0' end post_install do |installer| diff --git a/messaging/integration_test/build.gradle b/messaging/integration_test/build.gradle index 1a463ad5eb..9bbc92c059 100644 --- a/messaging/integration_test/build.gradle +++ b/messaging/integration_test/build.gradle @@ -73,10 +73,15 @@ android { proguardFile file('proguard.pro') } } + + lintOptions { + abortOnError false + } } apply from: "$gradle.firebase_cpp_sdk_dir/Android/firebase_dependencies.gradle" firebaseCpp.dependencies { + functions messaging } diff --git a/messaging/integration_test/functions/firebase.json b/messaging/integration_test/functions/firebase.json new file mode 100644 index 0000000000..3313edb534 --- /dev/null +++ b/messaging/integration_test/functions/firebase.json @@ -0,0 +1,4 @@ +{ + "functions": { + } +} diff --git a/messaging/integration_test/functions/functions/index.js b/messaging/integration_test/functions/functions/index.js new file mode 100644 index 0000000000..62e5a7ae8f --- /dev/null +++ b/messaging/integration_test/functions/functions/index.js @@ -0,0 +1,52 @@ +/** + * Copyright 2024 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. + */ + +// Import the Firebase SDK for Google Cloud Functions. +const functions = require('firebase-functions'); +// Import and initialize the Firebase Admin SDK. +const admin = require('firebase-admin'); +admin.initializeApp(); + +// Import the FCM SDK +const messaging = admin.messaging(); + +exports.sendMessage = functions.https.onCall(async (data) => { + const {sendTo, isToken, notificationTitle, notificationBody, messageFields} = + data; + + const message = { + notification: { + title: notificationTitle, + body: notificationBody, + }, + data: messageFields, + }; + + if (isToken) { + message.token = sendTo; + } else { + message.topic = sendTo; + } + + try { + const response = await messaging.send(message); + console.log('Successfully sent message:', response); + return response; // Optionally return the FCM response to the client + } catch (error) { + console.error('Error sending message:', error); + throw new functions.https.HttpsError('internal', error.message); + } +}); diff --git a/messaging/integration_test/functions/functions/package.json b/messaging/integration_test/functions/functions/package.json new file mode 100644 index 0000000000..09c9edd38d --- /dev/null +++ b/messaging/integration_test/functions/functions/package.json @@ -0,0 +1,13 @@ +{ + "name": "messaging-functions", + "description": "Firebase Functions to send Cloud Messages", + "engines": { + "node": "16" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^12.1.1", + "firebase-functions": "^5.0.1" + }, + "private": true +} diff --git a/messaging/integration_test/src/integration_test.cc b/messaging/integration_test/src/integration_test.cc index 74b70c5698..453d5b5c7c 100644 --- a/messaging/integration_test/src/integration_test.cc +++ b/messaging/integration_test/src/integration_test.cc @@ -15,13 +15,18 @@ #include #include +#include #include #include #include #include +#include +#include +#include #include "app_framework.h" // NOLINT #include "firebase/app.h" +#include "firebase/functions.h" #include "firebase/messaging.h" #include "firebase/util.h" #include "firebase_test_framework.h" // NOLINT @@ -48,13 +53,6 @@ namespace firebase_testapp_automated { -// Your Firebase project's Server Key for Cloud Messaging goes here. -// You can get this from Firebase Console, in your Project settings under Cloud -// Messaging. -const char kFcmServerKey[] = "REPLACE_WITH_YOUR_SERVER_KEY"; - -const char kRestEndpoint[] = "https://fcm.googleapis.com/fcm/send"; - const char kNotificationLinkKey[] = "gcm.n.link"; const char kTestLink[] = "https://this-is-a-test-link/"; @@ -91,14 +89,21 @@ class FirebaseMessagingTest : public FirebaseTest { const std::map& message_fields, std::string* request_out, std::map* headers_out); - // Send a message previously created by CreateTestMessage. - void SendTestMessage(const std::string& request, - const std::map& headers); - // Convenience method combining the above. - void SendTestMessage( + // Send a message to a token or topic by using Firebase Functions to trigger + // it. + void SendTestMessageToToken( const char* send_to, const char* notification_title, const char* notification_body, const std::map& message_fields); + void SendTestMessageToTopic( + const char* send_to, const char* notification_title, + const char* notification_body, + const std::map& message_fields); + // Called by the above. + void SendTestMessage( + const char* send_to, bool is_token, const char* notification_title, + const char* notification_body, + const std::map& message_fields); // Get a unique message ID so we can confirm the correct message is being // received. @@ -122,6 +127,8 @@ class FirebaseMessagingTest : public FirebaseTest { static firebase::messaging::PollableListener* shared_listener_; static std::string* shared_token_; static bool is_desktop_stub_; + + static firebase::functions::Functions* functions_; }; const char kObtainedPermissionKey[] = "messaging_got_permission"; @@ -131,6 +138,7 @@ firebase::App* FirebaseMessagingTest::shared_app_ = nullptr; firebase::messaging::PollableListener* FirebaseMessagingTest::shared_listener_ = nullptr; bool FirebaseMessagingTest::is_desktop_stub_; +firebase::functions::Functions* FirebaseMessagingTest::functions_ = nullptr; void FirebaseMessagingTest::SetUpTestSuite() { LogDebug("Initialize Firebase App."); @@ -177,6 +185,7 @@ void FirebaseMessagingTest::SetUpTestSuite() { << initializer.InitializeLastResult().error_message(); LogDebug("Successfully initialized Firebase Cloud Messaging."); + functions_ = firebase::functions::Functions::GetInstance(shared_app_); is_desktop_stub_ = false; #if !defined(ANDROID) && !(defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) is_desktop_stub_ = true; @@ -193,6 +202,11 @@ void FirebaseMessagingTest::TearDownTestSuite() { LogDebug("Shutdown Firebase Cloud Messaging."); firebase::messaging::Terminate(); + if (functions_) { + LogDebug("Shutdown the Functions library."); + delete functions_; + functions_ = nullptr; + } LogDebug("Shutdown Firebase App."); delete shared_app_; shared_app_ = nullptr; @@ -220,68 +234,41 @@ std::string FirebaseMessagingTest::GetUniqueMessageId() { return std::string(buffer); } -// send_to can be a FCM token or a topic subscription. -bool FirebaseMessagingTest::CreateTestMessage( +void FirebaseMessagingTest::SendTestMessageToToken( const char* send_to, const char* notification_title, const char* notification_body, - const std::map& message_fields, - std::string* request_out, std::map* headers_out) { - if (is_desktop_stub_) { - // Don't send HTTP requests in stub mode. - return false; - } - if (strcasecmp(kFcmServerKey, "replace_with_your_server_key") == 0) { - LogWarning( - "Please put your Firebase Cloud Messaging server key in " - "kFcmServerKey."); - LogWarning("Without a server key, most of these tests will fail."); - } - std::map headers; - headers.insert(std::make_pair("Content-type", "application/json")); - headers.insert( - std::make_pair("Authorization", std::string("key=") + kFcmServerKey)); - std::string request; // Build a JSON request. - request += "{\"notification\":{\"title\":\""; - request += notification_title ? notification_title : ""; - request += "\",\"body\":\""; - request += notification_body ? notification_body : ""; - request += "\"},\"data\":{"; - for (auto i = message_fields.begin(); i != message_fields.end(); ++i) { - if (i != message_fields.begin()) request += ","; - request += "\""; - request += i->first; - request += "\":\""; - request += i->second; - request += "\""; - } - request += "}, \"to\":\""; - request += send_to; - // Messages will expire after 5 minutes, so if there are stale/leftover - // messages from a previous run, just wait 5 minutes and try again. - request += "\", \"time_to_live\":300}"; - *request_out = request; - *headers_out = headers; - return true; + const std::map& message_fields) { + SendTestMessage(send_to, true, notification_title, notification_body, + message_fields); } -void FirebaseMessagingTest::SendTestMessage( +void FirebaseMessagingTest::SendTestMessageToTopic( const char* send_to, const char* notification_title, const char* notification_body, const std::map& message_fields) { - std::string request; - std::map headers; - EXPECT_TRUE(CreateTestMessage(send_to, notification_title, notification_body, - message_fields, &request, &headers)); - SendTestMessage(request, headers); + SendTestMessage(send_to, false, notification_title, notification_body, + message_fields); } void FirebaseMessagingTest::SendTestMessage( - const std::string& request, - const std::map& headers) { - LogDebug("Request: %s", request.c_str()); - LogDebug("Triggering FCM message from server..."); - EXPECT_TRUE( - SendHttpPostRequest(kRestEndpoint, headers, request, nullptr, nullptr)); + const char* send_to, bool is_token, const char* notification_title, + const char* notification_body, + const std::map& message_fields) { + firebase::Variant data(firebase::Variant::EmptyMap()); + data.map()["sendTo"] = firebase::Variant(send_to); + data.map()["isToken"] = firebase::Variant(is_token); + data.map()["notificationTitle"] = firebase::Variant(notification_title); + data.map()["notificationBody"] = firebase::Variant(notification_body); + data.map()["messageFields"] = firebase::Variant(message_fields); + + firebase::functions::HttpsCallableReference ref = + functions_->GetHttpsCallable("sendMessage"); + + LogDebug("Triggering FCM message via Firebase Functions..."); + firebase::Future future = + ref.Call(data); + + EXPECT_TRUE(WaitForCompletion(future, "SendTestMessage")); } bool FirebaseMessagingTest::WaitForToken(int timeout) { @@ -372,6 +359,10 @@ TEST_F(FirebaseMessagingTest, TestReceiveToken) { EXPECT_NE(*shared_token_, ""); FLAKY_TEST_SECTION_END(); + + // Add in a small delay to make sure that the backend is ready to send + // targeted messages to the new device. + std::this_thread::sleep_for(std::chrono::seconds(2)); } TEST_F(FirebaseMessagingTest, TestSubscribeAndUnsubscribe) { @@ -391,121 +382,6 @@ TEST_F(FirebaseMessagingTest, TestSubscribeAndUnsubscribe) { firebase::messaging::Unsubscribe("SubscribeTest"), "Unsubscribe")); } -static std::string ConstructHtmlToSendMessage( - const std::string& request, - const std::map& headers, int delay_seconds) { - // Generate some simple HTML/Javascript to pause a few seconds, then send the - // POST request via XMLHttpRequest. - std::string h; - h += ""; - return h; -} - -TEST_F(FirebaseMessagingTest, TestNotification) { - TEST_REQUIRES_USER_INTERACTION; - SKIP_TEST_ON_DESKTOP; - - EXPECT_TRUE(RequestPermission()); - EXPECT_TRUE(WaitForToken()); - - // To test notifications, this test app must be running in the background. To - // accomplish this, switch over to the device's web browser, loading an HTML - // page that will, after a short delay, send the FCM message request to the - // app in the background. This will produce the system notification that you - // can then click on to go back into the app and continue the test. - - std::string unique_id = GetUniqueMessageId(); - std::string token = *shared_token_; - const char kNotificationTitle[] = "FCM Integration Test"; - const char kNotificationBody[] = "Test notification, open to resume testing."; - std::string value; - if (!GetPersistentString(kTestingNotificationKey, &value) || value.empty()) { - // If the notification test is already in progress, just go straight to the - // waiting part. This can happen if you wait too long to click on the - // notification and the app is no longer running in the background. - std::string request; - std::map headers; - std::map message_fields = { - {"message", "This is a notification."}, - {"unique_id", unique_id}, -#if defined(__ANDROID__) - // Duplicate notification.title and notification.body here, see - // below for why. - {"notification_title", kNotificationTitle}, - {"notification_body", kNotificationBody}, -#endif // defined(__ANDROID__) - }; - EXPECT_TRUE(CreateTestMessage(shared_token_->c_str(), kNotificationTitle, - kNotificationBody, message_fields, &request, - &headers)); - std::string html = ConstructHtmlToSendMessage(request, headers, 5); - // We now have some HTML/Javascript to send the message request. Embed it in - // a data: url so we can try receiving a message with the app in the - // background. - // Encode the HTML into base64. - std::string html_encoded; - EXPECT_TRUE(Base64Encode(html, &html_encoded)); - std::string url = std::string("data:text/html;base64,") + html_encoded; - LogInfo("Opening browser to trigger FCM message."); - if (OpenUrlInBrowser(url.c_str())) { - SetPersistentString(kTestingNotificationKey, "1"); - } else { - FAIL() << "Failed to open URL in browser."; - } - } - SetPersistentString(kTestingNotificationKey, nullptr); - LogDebug("Waiting for message."); - firebase::messaging::Message message; - EXPECT_TRUE(WaitForMessage(&message, 120)); - EXPECT_EQ(message.data["unique_id"], unique_id); - EXPECT_TRUE(message.notification_opened); - -#if defined(__ANDROID__) - // On Android, if the app is running in the background, FCM does not deliver - // both the "notification" and the "data". So for our purposes, duplicate the - // notification fields we are checking into the data fields so we can still - // check that it's correct. - EXPECT_EQ(message.notification, nullptr); - EXPECT_EQ(message.data["notification_title"], kNotificationTitle); - EXPECT_EQ(message.data["notification_body"], kNotificationBody); -#else - // On iOS, we do get the notification. - EXPECT_NE(message.notification, nullptr); - if (message.notification) { - EXPECT_EQ(message.notification->title, kNotificationTitle); - EXPECT_EQ(message.notification->body, kNotificationBody); - } -#endif // defined(__ANDROID__) -} - TEST_F(FirebaseMessagingTest, TestSendMessageToToken) { TEST_REQUIRES_USER_INTERACTION_ON_IOS; SKIP_TEST_ON_DESKTOP; @@ -520,11 +396,11 @@ TEST_F(FirebaseMessagingTest, TestSendMessageToToken) { std::string unique_id = GetUniqueMessageId(); const char kNotificationTitle[] = "Token Test"; const char kNotificationBody[] = "Token Test notification body"; - SendTestMessage(shared_token()->c_str(), kNotificationTitle, - kNotificationBody, - {{"message", "Hello, world!"}, - {"unique_id", unique_id}, - {kNotificationLinkKey, kTestLink}}); + SendTestMessageToToken(shared_token()->c_str(), kNotificationTitle, + kNotificationBody, + {{"message", "Hello, world!"}, + {"unique_id", unique_id}, + {kNotificationLinkKey, kTestLink}}); LogDebug("Waiting for message."); firebase::messaging::Message message; EXPECT_TRUE(WaitForMessage(&message)); @@ -561,10 +437,10 @@ TEST_F(FirebaseMessagingTest, TestSendMessageToTopic) { : "00"); std::string topic = "FCMTestTopic" + unique_id_tag; firebase::Future sub = firebase::messaging::Subscribe(topic.c_str()); - WaitForCompletion(sub, "Subscribe"); - SendTestMessage(("/topics/" + topic).c_str(), kNotificationTitle, - kNotificationBody, - {{"message", "Hello, world!"}, {"unique_id", unique_id}}); + WaitForCompletion(sub, ("Subscribe " + topic).c_str()); + SendTestMessageToTopic( + ("/topics/" + topic).c_str(), kNotificationTitle, kNotificationBody, + {{"message", "Hello, world!"}, {"unique_id", unique_id}}); firebase::messaging::Message message; EXPECT_TRUE(WaitForMessage(&message)); @@ -579,8 +455,9 @@ TEST_F(FirebaseMessagingTest, TestSendMessageToTopic) { // Ensure that we *don't* receive a message now. unique_id = GetUniqueMessageId(); - SendTestMessage(("/topics/" + topic).c_str(), "Topic Title 2", "Topic Body 2", - {{"message", "Hello, world!"}, {"unique_id", unique_id}}); + SendTestMessageToTopic( + ("/topics/" + topic).c_str(), "Topic Title 2", "Topic Body 2", + {{"message", "Hello, world!"}, {"unique_id", unique_id}}); // If this returns true, it means we received a message but // shouldn't have. @@ -611,8 +488,9 @@ TEST_F(FirebaseMessagingTest, TestChangingListener) { std::string unique_id = GetUniqueMessageId(); const char kNotificationTitle[] = "New Listener Test"; const char kNotificationBody[] = "New Listener Test notification body"; - SendTestMessage(shared_token_->c_str(), kNotificationTitle, kNotificationBody, - {{"message", "Hello, world!"}, {"unique_id", unique_id}}); + SendTestMessageToToken( + shared_token_->c_str(), kNotificationTitle, kNotificationBody, + {{"message", "Hello, world!"}, {"unique_id", unique_id}}); LogDebug("Waiting for message."); firebase::messaging::Message message; EXPECT_TRUE(WaitForMessage(&message)); diff --git a/scripts/gha-encrypted/messaging/server_key.txt.gpg b/scripts/gha-encrypted/messaging/server_key.txt.gpg deleted file mode 100644 index b58b7f45a2..0000000000 --- a/scripts/gha-encrypted/messaging/server_key.txt.gpg +++ /dev/null @@ -1,3 +0,0 @@ -Œ   EIÊDÌ¢DÿÒÀ îfªéS?5‘Þ<Œ™?µÚÈ…š€Ñ zþFIÿuÈUXßp±-~y`‹F¤µ}§¿VRs”C‡µªö!ޟž‹€CÕ[ †WxîFdH´³tFÇ8Àá÷øl½ °®ªÆ¿üGùÕŽ¶ù&fni‚”¹ev ðÇ#ÒŽ£÷¼±›ÐõòÄ£eK2ú -à$S®t3|ìþ"Åœžs±9>ëû™û*kZ¸¥%$¦€$w£$e÷j -X'l¹Î (5ëe„­+ 1 \ No newline at end of file diff --git a/scripts/gha/integration_testing/build_testapps.json b/scripts/gha/integration_testing/build_testapps.json index fccdad0639..aa57936b3d 100755 --- a/scripts/gha/integration_testing/build_testapps.json +++ b/scripts/gha/integration_testing/build_testapps.json @@ -133,6 +133,7 @@ "tvos_target": "integration_test_tvos", "testapp_path": "messaging/integration_test", "frameworks": [ + "firebase_functions.xcframework", "firebase_messaging.xcframework", "firebase.xcframework" ], diff --git a/scripts/gha/lint_commenter.py b/scripts/gha/lint_commenter.py index f217e86f65..2ac787173b 100755 --- a/scripts/gha/lint_commenter.py +++ b/scripts/gha/lint_commenter.py @@ -127,7 +127,7 @@ def main(): '-H', header, '-H', 'Authorization: token %s' % args.token, request_url - ] + ([] if not args.verbose else ['-v'])).decode('utf-8') + ] + ([] if not args.verbose else ['-v'])).decode('utf-8', errors='replace') # Parse the diff to determine the whether each source line is touched. # Only lint lines that refer to parts of files that are diffed will be shown. # Information on what this means here: diff --git a/scripts/gha/restore_secrets.py b/scripts/gha/restore_secrets.py index 35e9ac072b..93aefcfd72 100644 --- a/scripts/gha/restore_secrets.py +++ b/scripts/gha/restore_secrets.py @@ -124,13 +124,6 @@ def main(argv): dlinks_project = os.path.join(repo_dir, "dynamic_links", "integration_test") _patch_main_src(dlinks_project, "REPLACE_WITH_YOUR_URI_PREFIX", uri_prefix) - if not FLAGS.apis or "messaging" in FLAGS.apis: - print("Attempting to patch Messaging server key.") - server_key_path = os.path.join(secrets_dir, "messaging", "server_key.txt.gpg") - server_key = _decrypt(server_key_path, passphrase) - messaging_project = os.path.join(repo_dir, "messaging", "integration_test") - _patch_main_src(messaging_project, "REPLACE_WITH_YOUR_SERVER_KEY", server_key) - if not FLAGS.apis or "app_check" in FLAGS.apis: print("Attempting to patch app check debug token.") app_check_token_path = os.path.join(