From 89c4c739a0d1f8c925e1269ee208b2b2c90e8864 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:24:21 -0400 Subject: [PATCH 001/219] chore(deps): bump libraries-bom from 26.10.0 to 26.12.0 (#796) Bumps [libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.10.0 to 26.12.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.10.0...v26.12.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 29bf60cd7..d85d97378 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.10.0 + 26.12.0 pom import From 3d260badec7e7e55b1e6defd8b66dd031c42201f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:26:55 +0000 Subject: [PATCH 002/219] chore(deps): bump maven-failsafe-plugin from 2.22.2 to 3.0.0 (#788) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d85d97378..ecb45926d 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 2.22.2 + 3.0.0 From de51a7fe6599a72a68cdf135b1bc432763956366 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:29:25 +0000 Subject: [PATCH 003/219] chore(deps): bump slf4j-api from 2.0.6 to 2.0.7 (#790) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ecb45926d..77d20c702 100644 --- a/pom.xml +++ b/pom.xml @@ -438,7 +438,7 @@ org.slf4j slf4j-api - 2.0.6 + 2.0.7 io.netty From 3179f82fdd82481f3fdde58da7fe4a26a4b4cff7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:32:25 +0000 Subject: [PATCH 004/219] chore(deps): bump netty.version from 4.1.90.Final to 4.1.91.Final (#793) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 77d20c702..8b1aca116 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.90.Final + 4.1.91.Final From 7957d316b33994cef898784be2130ff0b9066f8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:35:36 +0000 Subject: [PATCH 005/219] chore(deps): bump maven-surefire-plugin from 2.22.2 to 3.0.0 (#789) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8b1aca116..463228c56 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 2.22.2 + 3.0.0 ${skipUTs} From 22aadefb3e90f019f1172f55cfedf73c4881245b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 2 Jun 2023 12:22:44 -0400 Subject: [PATCH 006/219] Expose MultiDb API (#814) --- src/main/java/com/google/firebase/cloud/FirestoreClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index e1672ec34..e55d5c7e9 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -94,7 +94,7 @@ public static Firestore getFirestore(FirebaseApp app) { * instance. */ @NonNull - static Firestore getFirestore(FirebaseApp app, String database) { + public static Firestore getFirestore(FirebaseApp app, String database) { return getInstance(app, database).firestore; } @@ -108,7 +108,7 @@ static Firestore getFirestore(FirebaseApp app, String database) { * instance. */ @NonNull - static Firestore getFirestore(String database) { + public static Firestore getFirestore(String database) { return getFirestore(FirebaseApp.getInstance(), database); } From 60f97c582d975eade0f21b72e2ace24f97ff0b59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jun 2023 19:09:25 +0000 Subject: [PATCH 007/219] chore(deps): bump jacoco-maven-plugin from 0.8.8 to 0.8.10 (#809) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 463228c56..1e66dcb68 100644 --- a/pom.xml +++ b/pom.xml @@ -189,7 +189,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.10 pre-unit-test From abd0a8fb2ac4ede1c2a2ef3e3acc5bb4b49287f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jun 2023 19:11:29 +0000 Subject: [PATCH 008/219] chore(deps): bump maven-project-info-reports-plugin from 3.4.2 to 3.4.4 (#813) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1e66dcb68..6accbfcab 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ maven-project-info-reports-plugin - 3.4.2 + 3.4.4 From 06ebf346ba42cdb1574a447304cab49f382e0ec8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:58:39 -0400 Subject: [PATCH 009/219] chore(deps): bump libraries-bom from 26.12.0 to 26.16.0 (#817) Bumps [libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.12.0 to 26.16.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.12.0...v26.16.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6accbfcab..1da771e36 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.12.0 + 26.16.0 pom import From 167f16be4d4b8b0ab861b11e52bc7d4318382318 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 18:05:27 +0000 Subject: [PATCH 010/219] chore(deps): bump netty.version from 4.1.91.Final to 4.1.93.Final (#816) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1da771e36..10eacc245 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.91.Final + 4.1.93.Final From 17fff8374ccb1a40a7835df534cc6655d81afc34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:01:00 -0400 Subject: [PATCH 011/219] chore(deps): bump maven-failsafe-plugin from 3.0.0 to 3.1.0 (#820) Bumps [maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0...surefire-3.1.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 10eacc245..260dbfc2c 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.0.0 + 3.1.0 From 27ab2916720c56e1badfe147e38233197ec4ffe5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:04:37 +0000 Subject: [PATCH 012/219] chore(deps): bump maven-gpg-plugin from 3.0.1 to 3.1.0 (#823) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 260dbfc2c..048856fca 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ maven-gpg-plugin - 3.0.1 + 3.1.0 sign-artifacts From 090dd3afd1597c06a2b9b257d5e223f523579b1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:08:13 +0000 Subject: [PATCH 013/219] chore(deps): bump maven-source-plugin from 3.2.1 to 3.3.0 (#821) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 048856fca..a21a568fe 100644 --- a/pom.xml +++ b/pom.xml @@ -289,7 +289,7 @@ maven-source-plugin - 3.2.1 + 3.3.0 attach-sources From 25dfab675a4cda94e37ada0a4cc8998ca23523e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:10:41 +0000 Subject: [PATCH 014/219] chore(deps): bump maven-surefire-plugin from 3.0.0 to 3.1.0 (#822) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a21a568fe..dfd425ef0 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.0.0 + 3.1.0 ${skipUTs} From d1baee366cebf121aa6d5a62593c08cc26ce97a8 Mon Sep 17 00:00:00 2001 From: Doris-Ge Date: Thu, 8 Jun 2023 13:10:31 -0700 Subject: [PATCH 015/219] feat(fcm): Implement `sendEach`, `sendEachAsync`, `sendEachForMulticast` and `sendEachForMulticastAsync` (#785) (#815) * feat(fcm): Implement `sendEach`, `sendEachAsync`, `sendEachForMulticast` and `sendEachForMulticastAsync` (#785) * Add org.hamcrest as dependency for unit tests * Implement sendEach, sendEachAsync, sendEachForMulticast and sendEachForMulticastAsync `sendEach` vs `sendAll` 1. `sendEach` sends one HTTP request to V1 Send endpoint for each message in the array. `sendAll` sends only one HTTP request to V1 Batch Send endpoint to send all messages in the array. 2. `sendEach` calls `messagingClient.send` to send each message and constructs a `SendResponse` with the returned `messageId`. If `messagingClient.send` throws out an exception, `sendEach` will catch the exception and also turn it into a `SendResponse` with the exception in it. `sendEach` calls `ApiFutures.allAsList().get()` to execute all `messagingClient.send` calls asynchronously and wait for all of them to complete and construct a `BatchResponse` with all `SendResponse`s. Therefore, unlike `sendAll`, `sendEach` does not always throw an error for a total failure. It can also return a `BatchResponse` with only errors in it. `sendEachForMulticast` calls `sendEach` under the hood. `sendEachAsync` is the async version of `sendEach`. `sendEachForMulticastAsync` is the async version of `sendEachForMulticast`. * Add integration tests for batch-send re-implementation: testSendEach(), testSendFiveHundredWithSendEach(), testSendEachForMulticast() * Replace all "-- i.e." in the comments with "meaning that" * Fix the build errors caused by "Line is longer than 100 characters" * Fix the build errors caused by "package does not exist" * Address doc review comments --- pom.xml | 6 + .../firebase/messaging/FirebaseMessaging.java | 221 ++++++++++++- .../messaging/FirebaseMessagingIT.java | 97 ++++++ .../messaging/FirebaseMessagingTest.java | 306 +++++++++++++++++- 4 files changed, 618 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index dfd425ef0..0a67f6c14 100644 --- a/pom.xml +++ b/pom.xml @@ -482,5 +482,11 @@ 2.1.1 test + + org.hamcrest + hamcrest + 2.2 + test + diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 95366eaa7..882fec8fc 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -20,18 +20,22 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; /** * This class is the entry point for all server-side Firebase Cloud Messaging actions. @@ -91,7 +95,7 @@ public String send(@NonNull Message message) throws FirebaseMessagingException { * *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead * FCM performs all the necessary validations and emulates the send operation. The {@code dryRun} - * option is useful for determining whether an FCM registration has been deleted. However, it + * option is useful for determining whether an FCM registration has been deleted. However, it * cannot be used to validate APNs tokens. * * @param message A non-null {@link Message} to be sent. @@ -139,6 +143,191 @@ protected String execute() throws FirebaseMessagingException { }; } + /** + * Sends each message in the given list via Firebase Cloud Messaging. + * Unlike {@link #sendAll(List)}, this method makes an HTTP call for each message in the + * given array. + * + *

The list of responses obtained by calling {@link BatchResponse#getResponses()} on the return + * value is in the same order as the input list. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure, meaning that none of the messages in the list could be sent. Partial failures or + * no failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEach(@NonNull List messages) throws FirebaseMessagingException { + return sendEachOp(messages, false).call(); + } + + + /** + * Sends each message in the given list via Firebase Cloud Messaging. + * Unlike {@link #sendAll(List)}, this method makes an HTTP call for each message in the + * given array. + * + *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. The {@code dryRun} + * option is useful for determining whether an FCM registration has been deleted. But it cannot be + * used to validate APNs tokens. + * + *

The list of responses obtained by calling {@link BatchResponse#getResponses()} on the return + * value is in the same order as the input list. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure, meaning that none of the messages in the list could be sent. Partial failures or + * no failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEach( + @NonNull List messages, boolean dryRun) throws FirebaseMessagingException { + return sendEachOp(messages, dryRun).call(); + } + + /** + * Similar to {@link #sendEach(List)} but performs the operation asynchronously. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachAsync(@NonNull List messages) { + return sendEachOp(messages, false).callAsync(app); + } + + /** + * Similar to {@link #sendEach(List, boolean)} but performs the operation asynchronously. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachAsync(@NonNull List messages, boolean dryRun) { + return sendEachOp(messages, dryRun).callAsync(app); + } + + private CallableOperation sendEachOp( + final List messages, final boolean dryRun) { + final List immutableMessages = ImmutableList.copyOf(messages); + checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); + checkArgument(immutableMessages.size() <= 500, + "messages list must not contain more than 500 elements"); + + return new CallableOperation() { + @Override + protected BatchResponse execute() throws FirebaseMessagingException { + List> list = new ArrayList<>(); + for (Message message : immutableMessages) { + ApiFuture messageId = sendOpForSendResponse(message, dryRun).callAsync(app); + list.add(messageId); + } + try { + List responses = ApiFutures.allAsList(list).get(); + return new BatchResponseImpl(responses); + } catch (InterruptedException | ExecutionException e) { + throw new FirebaseMessagingException(ErrorCode.CANCELLED, SERVICE_ID); + } + } + }; + } + + private CallableOperation sendOpForSendResponse( + final Message message, final boolean dryRun) { + checkNotNull(message, "message must not be null"); + final FirebaseMessagingClient messagingClient = getMessagingClient(); + return new CallableOperation() { + @Override + protected SendResponse execute() { + try { + String messageId = messagingClient.send(message, dryRun); + return SendResponse.fromMessageId(messageId); + } catch (FirebaseMessagingException e) { + return SendResponse.fromException(e); + } + } + }; + } + + /** + * Sends the given multicast message to all the FCM registration tokens specified in it. + * + *

This method uses the {@link #sendEach(List)} API under the hood to send the given + * message to all the target recipients. The list of responses obtained by calling + * {@link BatchResponse#getResponses()} on the return value is in the same order as the + * tokens in the {@link MulticastMessage}. + * + * @param message A non-null {@link MulticastMessage} + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure, meaning that none of the messages in the list could be sent. Partial failures or + * no failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEachForMulticast( + @NonNull MulticastMessage message) throws FirebaseMessagingException { + return sendEachForMulticast(message, false); + } + + /** + * Sends the given multicast message to all the FCM registration tokens specified in it. + * + *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. The {@code dryRun} + * option is useful for determining whether an FCM registration has been deleted. But it cannot be + * used to validate APNs tokens. + * + *

This method uses the {@link #sendEach(List)} API under the hood to send the given + * message to all the target recipients. The list of responses obtained by calling + * {@link BatchResponse#getResponses()} on the return value is in the same order as the + * tokens in the {@link MulticastMessage}. + * + * @param message A non-null {@link MulticastMessage}. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure, meaning that none of the messages in the list could be sent. Partial failures or + * no failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEachForMulticast(@NonNull MulticastMessage message, boolean dryRun) + throws FirebaseMessagingException { + checkNotNull(message, "multicast message must not be null"); + return sendEach(message.getMessageList(), dryRun); + } + + /** + * Similar to {@link #sendEachForMulticast(MulticastMessage)} but performs the operation + * asynchronously. + * + * @param message A non-null {@link MulticastMessage}. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachForMulticastAsync(@NonNull MulticastMessage message) { + return sendEachForMulticastAsync(message, false); + } + + /** + * Similar to {@link #sendEachForMulticast(MulticastMessage, boolean)} but performs the operation + * asynchronously. + * + * @param message A non-null {@link MulticastMessage}. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachForMulticastAsync( + @NonNull MulticastMessage message, boolean dryRun) { + checkNotNull(message, "multicast message must not be null"); + return sendEachAsync(message.getMessageList(), dryRun); + } + /** * Sends all the messages in the given list via Firebase Cloud Messaging. Employs batching to * send the entire list as a single RPC call. Compared to the {@link #send(Message)} method, this @@ -150,8 +339,10 @@ protected String execute() throws FirebaseMessagingException { * @param messages A non-null, non-empty list containing up to 500 messages. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for - * delivery. An exception here indicates a total failure -- i.e. none of the messages in the - * list could be sent. Partial failures are indicated by a {@link BatchResponse} return value. + * delivery. An exception here indicates a total failure, meaning that none of the messages in + * the list could be sent. Partial failures are indicated by a {@link BatchResponse} return + * value. + * @deprecated Use {@link #sendEach(List)} instead. */ public BatchResponse sendAll( @NonNull List messages) throws FirebaseMessagingException { @@ -175,8 +366,10 @@ public BatchResponse sendAll( * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for - * delivery. An exception here indicates a total failure -- i.e. none of the messages in the - * list could be sent. Partial failures are indicated by a {@link BatchResponse} return value. + * delivery. An exception here indicates a total failure, meaning that none of the messages in + * the list could be sent. Partial failures are indicated by a {@link BatchResponse} return + * value. + * @deprecated Use {@link #sendEach(List, boolean)} instead. */ public BatchResponse sendAll( @NonNull List messages, boolean dryRun) throws FirebaseMessagingException { @@ -187,8 +380,9 @@ public BatchResponse sendAll( * Similar to {@link #sendAll(List)} but performs the operation asynchronously. * * @param messages A non-null, non-empty list containing up to 500 messages. - * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. + * @deprecated Use {@link #sendEachAsync(List)} instead. */ public ApiFuture sendAllAsync(@NonNull List messages) { return sendAllAsync(messages, false); @@ -199,8 +393,9 @@ public ApiFuture sendAllAsync(@NonNull List messages) { * * @param messages A non-null, non-empty list containing up to 500 messages. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. - * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent, or when the emulation has finished. + * @deprecated Use {@link #sendEachAsync(List, boolean)} instead. */ public ApiFuture sendAllAsync( @NonNull List messages, boolean dryRun) { @@ -218,9 +413,10 @@ public ApiFuture sendAllAsync( * @param message A non-null {@link MulticastMessage} * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for - * delivery. An exception here indicates a total failure -- i.e. the messages could not be - * delivered to any recipient. Partial failures are indicated by a {@link BatchResponse} + * delivery. An exception here indicates a total failure, meaning that the messages could not + * be delivered to any recipient. Partial failures are indicated by a {@link BatchResponse} * return value. + * @deprecated Use {@link #sendEachForMulticast(MulticastMessage)} instead. */ public BatchResponse sendMulticast( @NonNull MulticastMessage message) throws FirebaseMessagingException { @@ -244,9 +440,10 @@ public BatchResponse sendMulticast( * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for - * delivery. An exception here indicates a total failure -- i.e. the messages could not be - * delivered to any recipient. Partial failures are indicated by a {@link BatchResponse} + * delivery. An exception here indicates a total failure, meaning that the messages could not + * be delivered to any recipient. Partial failures are indicated by a {@link BatchResponse} * return value. + * @deprecated Use {@link #sendEachForMulticast(MulticastMessage, boolean)} instead. */ public BatchResponse sendMulticast( @NonNull MulticastMessage message, boolean dryRun) throws FirebaseMessagingException { @@ -261,6 +458,7 @@ public BatchResponse sendMulticast( * @param message A non-null {@link MulticastMessage}. * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. + * @deprecated Use {@link #sendEachForMulticastAsync(MulticastMessage)} instead. */ public ApiFuture sendMulticastAsync(@NonNull MulticastMessage message) { return sendMulticastAsync(message, false); @@ -274,6 +472,7 @@ public ApiFuture sendMulticastAsync(@NonNull MulticastMessage mes * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. + * @deprecated Use {@link #sendEachForMulticastAsync(MulticastMessage, boolean)} instead. */ public ApiFuture sendMulticastAsync( @NonNull MulticastMessage message, boolean dryRun) { diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 9ae7bba24..bebae6e4d 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -96,6 +96,103 @@ public void testSendError() throws InterruptedException { } } + @Test + public void testSendEach() throws Exception { + List messages = new ArrayList<>(); + messages.add( + Message.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .setTopic("foo-bar") + .build()); + messages.add( + Message.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .setTopic("foo-bar") + .build()); + messages.add( + Message.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .setToken("not-a-token") + .build()); + + BatchResponse response = FirebaseMessaging.getInstance().sendEach(messages, true); + + assertEquals(2, response.getSuccessCount()); + assertEquals(1, response.getFailureCount()); + + List responses = response.getResponses(); + assertEquals(3, responses.size()); + assertTrue(responses.get(0).isSuccessful()); + String id = responses.get(0).getMessageId(); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); + + assertTrue(responses.get(1).isSuccessful()); + id = responses.get(1).getMessageId(); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); + + assertFalse(responses.get(2).isSuccessful()); + assertNull(responses.get(2).getMessageId()); + FirebaseMessagingException exception = responses.get(2).getException(); + assertNotNull(exception); + assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); + } + + @Test + public void testSendFiveHundredWithSendEach() throws Exception { + List messages = new ArrayList<>(); + for (int i = 0; i < 500; i++) { + messages.add(Message.builder().setTopic("foo-bar-" + (i % 10)).build()); + } + + BatchResponse response = FirebaseMessaging.getInstance().sendEach(messages, true); + + assertEquals(500, response.getResponses().size()); + assertEquals(500, response.getSuccessCount()); + assertEquals(0, response.getFailureCount()); + for (SendResponse sendResponse : response.getResponses()) { + if (!sendResponse.isSuccessful()) { + sendResponse.getException().printStackTrace(); + } + assertTrue(sendResponse.isSuccessful()); + String id = sendResponse.getMessageId(); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); + assertNull(sendResponse.getException()); + } + } + + @Test + public void testSendEachForMulticast() throws Exception { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .addToken("not-a-token") + .addToken("also-not-a-token") + .build(); + + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast( + multicastMessage, true); + + assertEquals(0, response.getSuccessCount()); + assertEquals(2, response.getFailureCount()); + assertEquals(2, response.getResponses().size()); + for (SendResponse sendResponse : response.getResponses()) { + assertFalse(sendResponse.isSuccessful()); + assertNull(sendResponse.getMessageId()); + assertNotNull(sendResponse.getException()); + } + } + @Test public void testSendAll() throws Exception { List messages = new ArrayList<>(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 8e8e79e07..dc07ce002 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -16,6 +16,9 @@ package com.google.firebase.messaging; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -27,13 +30,19 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.Nullable; + +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; + import org.junit.After; import org.junit.Test; @@ -46,6 +55,9 @@ public class FirebaseMessagingTest { private static final Message EMPTY_MESSAGE = Message.builder() .setTopic("test-topic") .build(); + private static final Message EMPTY_MESSAGE_2 = Message.builder() + .setTopic("test-topic2") + .build(); private static final MulticastMessage TEST_MULTICAST_MESSAGE = MulticastMessage.builder() .addToken("test-fcm-token1") .addToken("test-fcm-token2") @@ -262,6 +274,277 @@ public void testSendAsyncFailure() throws InterruptedException { assertFalse(client.isLastDryRun); } + @Test + public void testSendEachWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendEach(null); + fail("No error thrown for null message list"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEachWithEmptyList() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendEach(ImmutableList.of()); + fail("No error thrown for empty message list"); + } catch (IllegalArgumentException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEachWithTooManyMessages() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (int i = 0; i < 501; i++) { + listBuilder.add(Message.builder().setTopic("topic").build()); + } + + try { + messaging.sendEach(listBuilder.build(), false); + fail("No error thrown for too many messages in the list"); + } catch (IllegalArgumentException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEach() throws FirebaseMessagingException { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEach(messages); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachDryRun() throws FirebaseMessagingException { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEach(messages, true); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachFailure() throws FirebaseMessagingException { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + Map messageMap = new HashMap<>(); + messageMap.put(messages.get(0), SendResponse.fromMessageId("test")); + messageMap.put(messages.get(1), SendResponse.fromException(TEST_EXCEPTION)); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEach(messages); + + assertEquals(1, response.getFailureCount()); + assertEquals(1, response.getSuccessCount()); + assertEquals("test", response.getResponses().get(0).getMessageId()); + assertEquals(TEST_EXCEPTION, response.getResponses().get(1).getException()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachAsync() throws Exception { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachAsync(messages).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachAsyncDryRun() throws Exception { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachAsync(messages, true).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachAsyncFailure() throws Exception { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + Map messageMap = new HashMap<>(); + messageMap.put(messages.get(0), SendResponse.fromMessageId("test")); + messageMap.put(messages.get(1), SendResponse.fromException(TEST_EXCEPTION)); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachAsync(messages).get(); + + assertEquals(1, response.getFailureCount()); + assertEquals(1, response.getSuccessCount()); + assertEquals("test", response.getResponses().get(0).getMessageId()); + assertEquals(TEST_EXCEPTION, response.getResponses().get(1).getException()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendEachForMulticast(null); + fail("No error thrown for null multicast message"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEachForMulticast() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticast(TEST_MULTICAST_MESSAGE); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastDryRun() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticast(TEST_MULTICAST_MESSAGE, true); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastFailure() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticast(TEST_MULTICAST_MESSAGE); + + assertEquals(2, response.getFailureCount()); + assertEquals(0, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(TEST_EXCEPTION, response.getResponses().get(i).getException()); + } + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastAsync() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastAsyncDryRun() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticastAsync( + TEST_MULTICAST_MESSAGE, true).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastAsyncFailure() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + + assertEquals(2, response.getFailureCount()); + assertEquals(0, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(TEST_EXCEPTION, response.getResponses().get(i).getException()); + } + assertFalse(client.isLastDryRun); + } + @Test public void testSendAllWithNull() throws FirebaseMessagingException { MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); @@ -681,6 +964,7 @@ private static class MockFirebaseMessagingClient implements FirebaseMessagingCli private Message lastMessage; private List lastBatch; private boolean isLastDryRun; + private ImmutableMap messageMap; private MockFirebaseMessagingClient( String messageId, BatchResponse batchResponse, FirebaseMessagingException exception) { @@ -689,10 +973,20 @@ private MockFirebaseMessagingClient( this.exception = exception; } + private MockFirebaseMessagingClient( + Map messageMap, FirebaseMessagingException exception) { + this.messageMap = ImmutableMap.copyOf(messageMap); + this.exception = exception; + } + static MockFirebaseMessagingClient fromMessageId(String messageId) { return new MockFirebaseMessagingClient(messageId, null, null); } + static MockFirebaseMessagingClient fromMessageMap(Map messageMap) { + return new MockFirebaseMessagingClient(messageMap, null); + } + static MockFirebaseMessagingClient fromBatchResponse(BatchResponse batchResponse) { return new MockFirebaseMessagingClient(null, batchResponse, null); } @@ -702,13 +996,23 @@ static MockFirebaseMessagingClient fromException(FirebaseMessagingException exce } @Override + @Nullable public String send(Message message, boolean dryRun) throws FirebaseMessagingException { lastMessage = message; isLastDryRun = dryRun; if (exception != null) { throw exception; } - return messageId; + if (messageMap == null) { + return messageId; + } + if (!messageMap.containsKey(message)) { + return null; + } + if (messageMap.get(message).getException() != null) { + throw messageMap.get(message).getException(); + } + return messageMap.get(message).getMessageId(); } @Override From b940966a907e3527b61bd3eeab2027d9ea7c6bfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:21:40 -0400 Subject: [PATCH 016/219] chore(deps): bump maven-project-info-reports-plugin from 3.4.4 to 3.4.5 (#830) Bumps [maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 3.4.4 to 3.4.5. - [Commits](https://github.com/apache/maven-project-info-reports-plugin/compare/maven-project-info-reports-plugin-3.4.4...maven-project-info-reports-plugin-3.4.5) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-project-info-reports-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0a67f6c14..126bd3e4f 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ maven-project-info-reports-plugin - 3.4.4 + 3.4.5 From ed01081597bf8e52f439699a7e3909eba016eeb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:24:35 +0000 Subject: [PATCH 017/219] chore(deps): bump maven-surefire-plugin from 3.1.0 to 3.1.2 (#828) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 126bd3e4f..d326e9e06 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.1.0 + 3.1.2 ${skipUTs} From 4465a9141f0e747ccf1cd5cc770213382883636b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:27:39 +0000 Subject: [PATCH 018/219] chore(deps): bump maven-failsafe-plugin from 3.1.0 to 3.1.2 (#829) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d326e9e06..ba056d98e 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.1.0 + 3.1.2 From 0f414298eee57e56de8d49c29796f2adbd718dc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:05:19 -0400 Subject: [PATCH 019/219] chore(deps): bump netty-handler from 4.1.93.Final to 4.1.94.Final (#833) Bumps [netty-handler](https://github.com/netty/netty) from 4.1.93.Final to 4.1.94.Final. - [Commits](https://github.com/netty/netty/compare/netty-4.1.93.Final...netty-4.1.94.Final) --- updated-dependencies: - dependency-name: io.netty:netty-handler dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ba056d98e..878d83855 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.93.Final + 4.1.94.Final From adc7cf550497188c5ea41eeeea3cafed4e98626d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 17:24:15 +0000 Subject: [PATCH 020/219] chore(deps): bump libraries-bom from 26.16.0 to 26.17.0 (#831) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 878d83855..3d99466d1 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.16.0 + 26.17.0 pom import From 82da78bbd1b5d4a8b9bf1be9e2905c10fda39687 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 22 Jun 2023 15:37:55 -0400 Subject: [PATCH 021/219] [chore] Release 9.2.0 (#835) - Release 9.2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3d99466d1..ddd16db34 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.1.1 + 9.2.0 jar firebase-admin From 7a32102c61ade8e48649569c4c19c7b7aed01982 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:52:33 +0000 Subject: [PATCH 022/219] chore(deps): bump libraries-bom from 26.17.0 to 26.18.0 (#839) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ddd16db34..e56f31a13 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.17.0 + 26.18.0 pom import From 999742520a93cc8c232591802e640d5774ae5efd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:24:50 -0400 Subject: [PATCH 023/219] chore(deps): bump com.google.cloud:libraries-bom from 26.18.0 to 26.22.0 (#854) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.18.0 to 26.22.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.18.0...v26.22.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e56f31a13..0e672f5d1 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.18.0 + 26.22.0 pom import From 13a285723aab9584f587762a6b70c38386acf527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:28:32 +0000 Subject: [PATCH 024/219] chore(deps): bump netty.version from 4.1.94.Final to 4.1.96.Final (#847) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0e672f5d1..6e41a7757 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.94.Final + 4.1.96.Final From 315f5c0bccf07fa025fa38939400feca9950ba45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:54:59 +0000 Subject: [PATCH 025/219] chore(deps): bump com.google.cloud:libraries-bom from 26.22.0 to 26.24.0 (#864) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6e41a7757..ec70ea90c 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.22.0 + 26.24.0 pom import From a7ce4c396ba4472d812876692537d2a47efdc780 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:57:52 +0000 Subject: [PATCH 026/219] chore(deps): bump org.slf4j:slf4j-api from 2.0.7 to 2.0.9 (#858) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ec70ea90c..70763e2cc 100644 --- a/pom.xml +++ b/pom.xml @@ -438,7 +438,7 @@ org.slf4j slf4j-api - 2.0.7 + 2.0.9 io.netty From 2dcd91a1ddef8e1e4c2f56a722ec5bebec26154d Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Wed, 4 Oct 2023 19:08:00 +0200 Subject: [PATCH 027/219] Mark `sendAll` and `sendMulticast` variants as deprecated (#836) In d1baee36 all `sendAll` and `sendMulticast` variants were deprecated, however those methods were marked as deprecated only in javadoc but not using `@Deprecated`. This commit adds `@Deprecated` annotation where needed, in order to better signal the deprecation to developers. Co-authored-by: Lahiru Maramba --- .../com/google/firebase/messaging/FirebaseMessaging.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 882fec8fc..e16b6ac9d 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -344,6 +344,7 @@ public ApiFuture sendEachForMulticastAsync( * value. * @deprecated Use {@link #sendEach(List)} instead. */ + @Deprecated public BatchResponse sendAll( @NonNull List messages) throws FirebaseMessagingException { return sendAll(messages, false); @@ -371,6 +372,7 @@ public BatchResponse sendAll( * value. * @deprecated Use {@link #sendEach(List, boolean)} instead. */ + @Deprecated public BatchResponse sendAll( @NonNull List messages, boolean dryRun) throws FirebaseMessagingException { return sendAllOp(messages, dryRun).call(); @@ -384,6 +386,7 @@ public BatchResponse sendAll( * the messages have been sent. * @deprecated Use {@link #sendEachAsync(List)} instead. */ + @Deprecated public ApiFuture sendAllAsync(@NonNull List messages) { return sendAllAsync(messages, false); } @@ -397,6 +400,7 @@ public ApiFuture sendAllAsync(@NonNull List messages) { * the messages have been sent, or when the emulation has finished. * @deprecated Use {@link #sendEachAsync(List, boolean)} instead. */ + @Deprecated public ApiFuture sendAllAsync( @NonNull List messages, boolean dryRun) { return sendAllOp(messages, dryRun).callAsync(app); @@ -418,6 +422,7 @@ public ApiFuture sendAllAsync( * return value. * @deprecated Use {@link #sendEachForMulticast(MulticastMessage)} instead. */ + @Deprecated public BatchResponse sendMulticast( @NonNull MulticastMessage message) throws FirebaseMessagingException { return sendMulticast(message, false); @@ -445,6 +450,7 @@ public BatchResponse sendMulticast( * return value. * @deprecated Use {@link #sendEachForMulticast(MulticastMessage, boolean)} instead. */ + @Deprecated public BatchResponse sendMulticast( @NonNull MulticastMessage message, boolean dryRun) throws FirebaseMessagingException { checkNotNull(message, "multicast message must not be null"); @@ -460,6 +466,7 @@ public BatchResponse sendMulticast( * the messages have been sent. * @deprecated Use {@link #sendEachForMulticastAsync(MulticastMessage)} instead. */ + @Deprecated public ApiFuture sendMulticastAsync(@NonNull MulticastMessage message) { return sendMulticastAsync(message, false); } @@ -474,6 +481,7 @@ public ApiFuture sendMulticastAsync(@NonNull MulticastMessage mes * the messages have been sent. * @deprecated Use {@link #sendEachForMulticastAsync(MulticastMessage, boolean)} instead. */ + @Deprecated public ApiFuture sendMulticastAsync( @NonNull MulticastMessage message, boolean dryRun) { checkNotNull(message, "multicast message must not be null"); From 473500022d58fb4603de9fb0e06abf98c3e9e176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 17:17:06 +0000 Subject: [PATCH 028/219] chore(deps): bump netty.version from 4.1.96.Final to 4.1.99.Final (#865) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 70763e2cc..82eb1e4f9 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.96.Final + 4.1.99.Final From b4ab5903fb337b6023e331feb317f0866872c439 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 12 Oct 2023 12:48:24 -0400 Subject: [PATCH 029/219] Update `github.ref` value in `release.yml` (#867) - Fixes the release workflow to match the updates to `github.ref` - `github.ref` now returns a fully-formed value `refs/heads/...` - See https://github.blog/changelog/2023-09-13-github-actions-updates-to-github_ref-and-github-ref/ --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b49a453fc..94a42cfd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: # 3. with the label 'release:publish', and # 4. the title prefix '[chore] Release '. if: github.event.pull_request.merged && - github.ref == 'master' && + github.ref == 'refs/heads/master' && contains(github.event.pull_request.labels.*.name, 'release:publish') && startsWith(github.event.pull_request.title, '[chore] Release ') From 7822976286bb7ef2b1f91ef31315671e3b14dadf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:37:11 -0500 Subject: [PATCH 030/219] chore(deps): bump org.apache.maven.plugins:maven-javadoc-plugin (#881) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.5.0 to 3.6.3. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.5.0...maven-javadoc-plugin-3.6.3) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 82eb1e4f9..a141d2400 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.5.0 + 3.6.3 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.5.0 + 3.6.3 attach-javadocs From acc184093d3a7a588df1659e5f47d7c5d99f6c6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:42:13 +0000 Subject: [PATCH 031/219] chore(deps): bump org.jacoco:jacoco-maven-plugin from 0.8.10 to 0.8.11 (#870) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a141d2400..86de9cfd1 100644 --- a/pom.xml +++ b/pom.xml @@ -189,7 +189,7 @@ org.jacoco jacoco-maven-plugin - 0.8.10 + 0.8.11 pre-unit-test From 70c4808302d34fd927981aeae40f071767cb1739 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:44:25 +0000 Subject: [PATCH 032/219] chore(deps): bump com.google.cloud:libraries-bom from 26.24.0 to 26.28.0 (#884) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 86de9cfd1..f8f04ba9f 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.24.0 + 26.28.0 pom import From 27fc4bf68d0c0e534c1313271f8ab9e48d2f3817 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:50:49 +0000 Subject: [PATCH 033/219] chore(deps): bump netty.version from 4.1.99.Final to 4.1.101.Final (#885) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8f04ba9f..4a9191d8a 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.99.Final + 4.1.101.Final From 003b5544095de9e139f279a595520c03e057b22e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:43:13 -0500 Subject: [PATCH 034/219] chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin (#889) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.1.2 to 3.2.2. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.1.2...surefire-3.2.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4a9191d8a..b517accbe 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.1.2 + 3.2.2 ${skipUTs} From fe35c0fa27c33f0633741ecec8cb0580d1da2fbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:46:25 +0000 Subject: [PATCH 035/219] chore(deps): bump org.apache.maven.plugins:maven-project-info-reports-plugin (#886) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b517accbe..a289cac28 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ maven-project-info-reports-plugin - 3.4.5 + 3.5.0 From a4aac69d9365189d8db93fb0ce35f285408c99fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:49:13 +0000 Subject: [PATCH 036/219] chore(deps): bump org.codehaus.mojo:exec-maven-plugin (#887) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a289cac28..60fd1e115 100644 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.codehaus.mojo exec-maven-plugin - 3.1.0 + 3.1.1 test From 4357513d7b491eab27f8a66918e6c93ecc907e33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:52:03 +0000 Subject: [PATCH 037/219] chore(deps): bump org.apache.maven.plugins:maven-failsafe-plugin (#892) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 60fd1e115..c6c42d5d7 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.1.2 + 3.2.3 From 2f111fbd3d372be7bc0aab157b8781a7f80f1940 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:33:34 -0500 Subject: [PATCH 038/219] chore: Update integration test project setup instructions. (#877) * Update integration test project setup instructions. * fix: remove duplicate * fix: addresses some of the code review notes * fix: addresses remaining code review notes and some typos * Add note on 2nd rtdb reference. * fix: nits * fix: pencil * Added service account management note. --- CONTRIBUTING.md | 105 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3b65e101..2e6914c5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,27 +127,90 @@ mvn test Integration tests are also written using Junit4. They coexist with the unit tests in the `src/test` subdirectory. Integration tests follow the naming convention `*IT.java` (e.g. `DataTestIT.java`), which enables the Maven Surefire and Failsafe plugins to differentiate between the two types of -tests. Integration tests are executed against a real life Firebase project, and therefore -requires an Internet connection. - -Create a new project in the [Firebase console](https://console.firebase.google.com/) if you do -not already have one. Use a separate, dedicated project for integration tests since the test suite -makes a large number of writes to the Firebase realtime database. Download the service account -private key from the "Settings > Service Accounts" page of the project, and save it as -`integration_cert.json` at the root of the codebase. Grant your service account the `Firebase -Authentication Admin` role at -[Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is -required to ensure that exported user records contain the password hashes of the user accounts. -Also obtain the web API key of the project from the "Settings > General" page, and save it as -`integration_apikey.txt` at the root of the codebase. - -Some of the integration tests require an -[Identity Platform](https://cloud.google.com/identity-platform/) project with multi-tenancy -[enabled](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy). -An existing Firebase project can be upgraded to an Identity Platform project without losing any -functionality via the -[Identity Platform Marketplace Page](https://console.cloud.google.com/customer-identity). Note that -charges may be incurred for active users beyond the Identity Platform free tier. +tests. + +Integration tests are executed against a real life Firebase project. If you do not already +have one suitable for running the tests against, you can create a new project in the +[Firebase Console](https://console.firebase.google.com) following the setup guide below. +If you already have a Firebase project, you'll need to obtain credentials to communicate and +authorize access to your Firebase project: + +1. Service account certificate: This allows access to your Firebase project through a service account +which is required for all integration tests. This can be downloaded as a JSON file from the +**Settings > Service Accounts** tab of the Firebase console when you click the +**Generate new private key** button. Copy the file into the repo so it's available at +`integration_cert.json`. + > **Note:** Service accounts should be carefully managed and their keys should never be stored in publicly accessible source code or repositories. + + +2. Web API key: This allows for Auth sign-in needed for some Authentication and Tenant Management +integration tests. This is displayed in the **Settings > General** tab of the Firebase console +after enabling Authentication as described in the steps below. Copy it and save to a new text +file at `integration_apikey.txt`. + + +Set up your Firebase project as follows: + + +1. Enable Authentication: + 1. Go to the Firebase Console, and select **Authentication** from the **Build** menu. + 2. Click on **Get Started**. + 3. Select **Sign-in method > Add new provider > Email/Password** then enable both the + **Email/Password** and **Email link (passwordless sign-in)** options. + + +2. Enable Firestore: + 1. Go to the Firebase Console, and select **Firestore Database** from the **Build** menu. + 2. Click on the **Create database** button. You can choose to set up Firestore either in + the production mode or in the test mode. + + +3. Enable Realtime Database: + 1. Go to the Firebase Console, and select **Realtime Database** from the **Build** menu. + 2. Click on the **Create Database** button. You can choose to set up the Realtime Database + either in the locked mode or in the test mode. + + > **Note:** Integration tests are not run against the default Realtime Database reference and are + instead run against a database created at `https://{PROJECT_ID}.firebaseio.com`. + This second Realtime Database reference is created in the following steps. + + 3. In the **Data** tab click on the kebab menu (3 dots) and select **Create Database**. + 4. Enter your Project ID (Found in the **General** tab in **Account Settings**) as the + **Realtime Database reference**. Again, you can choose to set up the Realtime Database + either in the locked mode or in the test mode. + + +4. Enable Storage: + 1. Go to the Firebase Console, and select **Storage** from the **Build** menu. + 2. Click on the **Get started** button. You can choose to set up Cloud Storage + either in the production mode or in the test mode. + + +5. Enable the IAM API: + 1. Go to the [Google Cloud console](https://console.cloud.google.com) + and make sure your Firebase project is selected. + 2. Select **APIs & Services** from the main menu, and click the + **ENABLE APIS AND SERVICES** button. + 3. Search for and enable **Identity and Access Management (IAM) API** by Google Enterprise API. + + +6. Enable Tenant Management: + 1. Go to + [Google Cloud console | Identity Platform](https://console.cloud.google.com/customer-identity/) + and if it is not already enabled, click **Enable**. + 2. Then + [enable multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy) + for your project. + + +7. Ensure your service account has the **Firebase Authentication Admin** role. This is required +to ensure that exported user records contain the password hashes of the user accounts: + 1. Go to [Google Cloud console | IAM & admin](https://console.cloud.google.com/iam-admin). + 2. Find your service account in the list. If not added click the pencil icon to edit its + permissions. + 3. Click **ADD ANOTHER ROLE** and choose **Firebase Authentication Admin**. + 4. Click **SAVE**. + Now run the following command to invoke the integration test suite: From 5609fb45739222bd0cad25401dad8b7e4c287a64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:48:50 -0500 Subject: [PATCH 039/219] chore(deps): bump com.google.cloud:libraries-bom from 26.28.0 to 26.31.0 (#907) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.28.0 to 26.31.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.28.0...v26.31.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c6c42d5d7..a6804d543 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.28.0 + 26.31.0 pom import From cbf68e5642d99119996219e9ca5bc10c983230dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:51:35 +0000 Subject: [PATCH 040/219] chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin (#903) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a6804d543..3321dd105 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.2.2 + 3.2.5 ${skipUTs} From 67e8435f7f5289863eb3cba8222a0160846ce7c9 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:28:41 -0500 Subject: [PATCH 041/219] [chore] Update Github action workflows to fix node version and `set-output` deprecation warnings (#905) * Update Github action workflows to fix node version and `set-output` deprecation warnings * revert upload-artifact to v1 * revert upload-artifact to v1 * fix: JDK version syntax --- .github/scripts/publish_preflight_check.sh | 13 +++++---- .github/workflows/ci.yml | 5 ++-- .github/workflows/nightly.yml | 9 +++--- .github/workflows/release.yml | 32 ++++++++++------------ 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh index 7a191518d..7c8e384e2 100755 --- a/.github/scripts/publish_preflight_check.sh +++ b/.github/scripts/publish_preflight_check.sh @@ -69,7 +69,7 @@ if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then fi echo_info "Extracted release version: ${RELEASE_VERSION}" -echo "::set-output name=version::v${RELEASE_VERSION}" +echo "version=v${RELEASE_VERSION}" >> $GITHUB_OUTPUT echo_info "" @@ -132,12 +132,13 @@ readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` echo "$CHANGELOG" # Parse and preformat the text to handle multi-line output. -# See https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 +# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#example-of-a-multiline-string +# and https://github.com/github/docs/issues/21529#issue-1418590935 FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" -echo "::set-output name=changelog::${FILTERED_CHANGELOG}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\''/'"'}" +echo "changelog<> $GITHUB_OUTPUT +echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT +echo "CHANGELOGEOF" >> $GITHUB_OUTPUT echo "" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb09f08c6..39948e9e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,11 +24,12 @@ jobs: java-version: [8, 11, 17] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: + distribution: 'zulu' java-version: ${{ matrix.java-version }} # Does the following: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index dd3110c46..b29cd2fc4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -29,14 +29,15 @@ jobs: steps: - name: Checkout source for staging - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: ${{ github.event.client_payload.ref || github.ref }} - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - name: Set up JDK 8 + uses: actions/setup-java@v4 with: - java-version: 1.8 + distribution: 'zulu' + java-version: 8 - name: Compile, test and package run: ./.github/scripts/package_artifacts.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 94a42cfd5..c8a7050d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,14 +40,15 @@ jobs: # via the 'ref' client parameter. steps: - name: Checkout source for staging - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: ${{ github.event.client_payload.ref || github.ref }} - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - name: Set up JDK 8 + uses: actions/setup-java@v4 with: - java-version: 1.8 + distribution: 'zulu' + java-version: 8 - name: Compile, test and package run: ./.github/scripts/package_artifacts.sh @@ -80,12 +81,13 @@ jobs: steps: - name: Checkout source for publish - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - name: Set up JDK 8 + uses: actions/setup-java@v4 with: - java-version: 1.8 + distribution: 'zulu' + java-version: 8 - name: Publish preflight check id: preflight @@ -99,19 +101,13 @@ jobs: NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} - # We pull this action from a custom fork of a contributor until - # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of - # this action does not support the "body" parameter. + # See: https://cli.github.com/manual/gh_release_create - name: Create release tag - uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.preflight.outputs.version }} - release_name: Firebase Admin Java SDK ${{ steps.preflight.outputs.version }} - body: ${{ steps.preflight.outputs.changelog }} - draft: false - prerelease: false + run: gh release create ${{ steps.preflight.outputs.version }} + --title "Firebase Admin Java SDK ${{ steps.preflight.outputs.version }}" + --notes '${{ steps.preflight.outputs.changelog }}' # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - name: Post to Twitter From 7e61d864209c631fa306e575e1254c39d4e77cd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:41:04 +0000 Subject: [PATCH 042/219] chore(deps): bump com.google.api-client:google-api-client-bom (#911) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3321dd105..09e73a644 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.2.0 + 2.3.0 pom import From 8214d8b2341d53899f8bf0b99ff789754f89e5f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:57:43 -0500 Subject: [PATCH 043/219] chore(deps): bump netty.version from 4.1.101.Final to 4.1.107.Final (#913) Bumps `netty.version` from 4.1.101.Final to 4.1.107.Final. Updates `io.netty:netty-codec-http` from 4.1.101.Final to 4.1.107.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.107.Final) Updates `io.netty:netty-handler` from 4.1.101.Final to 4.1.107.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.107.Final) Updates `io.netty:netty-transport` from 4.1.101.Final to 4.1.107.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.107.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09e73a644..a3e60453b 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.101.Final + 4.1.107.Final From 4b01900164a7925612c2fcf54ab3d3c508ae5fda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 20:59:47 +0000 Subject: [PATCH 044/219] chore(deps): bump org.slf4j:slf4j-api from 2.0.9 to 2.0.12 (#910) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a3e60453b..816fca592 100644 --- a/pom.xml +++ b/pom.xml @@ -438,7 +438,7 @@ org.slf4j slf4j-api - 2.0.9 + 2.0.12 io.netty From dbc10e85bc2b24a19e7f80c0f66a7b57357115d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 21:14:31 +0000 Subject: [PATCH 045/219] chore(deps): bump org.apache.maven.plugins:maven-compiler-plugin (#897) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 816fca592..da047fbed 100644 --- a/pom.xml +++ b/pom.xml @@ -270,7 +270,7 @@ maven-compiler-plugin - 3.11.0 + 3.12.1 1.8 1.8 From fd4595c8289aeed4c14d5c714ad5d7b6f6b794c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:01:50 -0500 Subject: [PATCH 046/219] chore(deps): bump org.apache.maven.plugins:maven-failsafe-plugin (#914) Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.2.3 to 3.2.5. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.3...surefire-3.2.5) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index da047fbed..131c6f3a8 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.2.3 + 3.2.5 From bd6b207a664aca510a3d78ac8d729e6a557b85d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 20:03:57 +0000 Subject: [PATCH 047/219] chore(deps): bump com.google.cloud:libraries-bom from 26.31.0 to 26.32.0 (#915) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 131c6f3a8..8c6df98a5 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.31.0 + 26.32.0 pom import From 4e7c425489d0f887cd78b904bfc7b4275a7385b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:48:32 -0400 Subject: [PATCH 048/219] chore(deps): bump io.netty:netty-codec-http (#927) Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.107.Final to 4.1.108.Final. - [Commits](https://github.com/netty/netty/compare/netty-4.1.107.Final...netty-4.1.108.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8c6df98a5..3218d8f2f 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.107.Final + 4.1.108.Final From 12a6c3698d452215f8444de776ee711efa8d18fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:50:51 +0000 Subject: [PATCH 049/219] chore(deps): bump org.apache.maven.plugins:maven-gpg-plugin (#928) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3218d8f2f..f6a329ebf 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ maven-gpg-plugin - 3.1.0 + 3.2.2 sign-artifacts From 3c09099ee430eb36fd2280c3e853208f8e8ba1b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:53:21 +0000 Subject: [PATCH 050/219] chore(deps): bump com.google.cloud:libraries-bom from 26.32.0 to 26.34.0 (#921) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f6a329ebf..59d80a14d 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.32.0 + 26.34.0 pom import From 5cf06b0addafdad3431c745bc1d467b0b9d98407 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:55:58 +0000 Subject: [PATCH 051/219] chore(deps): bump org.codehaus.mojo:exec-maven-plugin (#918) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 59d80a14d..69392b00a 100644 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.codehaus.mojo exec-maven-plugin - 3.1.1 + 3.2.0 test From 3df58ccfc88dea4afa6519284be35ee5fe6c3de5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:59:13 +0000 Subject: [PATCH 052/219] chore(deps): bump com.google.api-client:google-api-client-bom (#926) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 69392b00a..40458c83e 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.3.0 + 2.4.0 pom import From 190e6afb100dfdcb99c0d37ee0c1a878fd8ddac4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:12:17 -0400 Subject: [PATCH 053/219] chore(deps): bump org.apache.maven.plugins:maven-compiler-plugin (#930) Bumps [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.12.1 to 3.13.0. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.12.1...maven-compiler-plugin-3.13.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 40458c83e..96a64d4a5 100644 --- a/pom.xml +++ b/pom.xml @@ -270,7 +270,7 @@ maven-compiler-plugin - 3.12.1 + 3.13.0 1.8 1.8 From bb4900f8854f4606967459e9d650864b80b817b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:14:39 +0000 Subject: [PATCH 054/219] chore(deps): bump com.google.cloud:libraries-bom from 26.34.0 to 26.36.0 (#932) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 96a64d4a5..4ab9b6065 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.34.0 + 26.36.0 pom import From c044a9c55f838d4ffd7a8429ea5e5d7d830f3b3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:36:39 +0000 Subject: [PATCH 055/219] chore(deps): bump org.slf4j:slf4j-api from 2.0.12 to 2.0.13 (#937) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4ab9b6065..d52fd09aa 100644 --- a/pom.xml +++ b/pom.xml @@ -438,7 +438,7 @@ org.slf4j slf4j-api - 2.0.12 + 2.0.13 io.netty From de76f4e85e6ed9947cdcc98e0e428a6da0d43d52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:40:09 +0000 Subject: [PATCH 056/219] chore(deps): bump org.apache.maven.plugins:maven-source-plugin (#936) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d52fd09aa..09eea59e2 100644 --- a/pom.xml +++ b/pom.xml @@ -289,7 +289,7 @@ maven-source-plugin - 3.3.0 + 3.3.1 attach-sources From b5251dd20f7407d6d8f61e2b0220bb9ed7698f88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:44:30 +0000 Subject: [PATCH 057/219] chore(deps): bump com.google.cloud:libraries-bom from 26.36.0 to 26.37.0 (#935) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09eea59e2..13399d553 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.36.0 + 26.37.0 pom import From 451a4e3532bb7e9f70c38dda586e5f777b856460 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:48:07 +0000 Subject: [PATCH 058/219] chore(deps): bump org.jacoco:jacoco-maven-plugin from 0.8.11 to 0.8.12 (#934) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13399d553..51239b23b 100644 --- a/pom.xml +++ b/pom.xml @@ -189,7 +189,7 @@ org.jacoco jacoco-maven-plugin - 0.8.11 + 0.8.12 pre-unit-test From 06bf0b3ef471cba09aa56a27be20761560ff9aac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 16:24:06 -0400 Subject: [PATCH 059/219] chore(deps): bump org.apache.maven.plugins:maven-gpg-plugin (#940) Bumps [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 3.2.2 to 3.2.4. - [Release notes](https://github.com/apache/maven-gpg-plugin/releases) - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.2...maven-gpg-plugin-3.2.4) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 51239b23b..3a59e7d01 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ maven-gpg-plugin - 3.2.2 + 3.2.4 sign-artifacts From 907344b102d749db157dcebd49278761612e1944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 20:26:54 +0000 Subject: [PATCH 060/219] chore(deps): bump com.google.api-client:google-api-client-bom (#939) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3a59e7d01..71f813396 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.4.0 + 2.4.1 pom import From 307a28b8e8d61aeda8912e36bbeede287a110130 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 20:29:37 +0000 Subject: [PATCH 061/219] chore(deps): bump netty.version from 4.1.108.Final to 4.1.109.Final (#938) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 71f813396..39257c4cb 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.108.Final + 4.1.109.Final From d7e0470e44a2cd3a103cf18a7ad6185bddb0c2b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 13:29:24 -0400 Subject: [PATCH 062/219] chore(deps): bump com.google.cloud:libraries-bom from 26.37.0 to 26.38.0 (#942) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.37.0 to 26.38.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.37.0...v26.38.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 39257c4cb..33d89ee67 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.37.0 + 26.38.0 pom import From bad0201c4c8decd0878f7238b9d1521d0d59fc72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 11:25:35 -0400 Subject: [PATCH 063/219] chore(deps): bump com.google.api-client:google-api-client-bom (#946) Bumps [com.google.api-client:google-api-client-bom](https://github.com/googleapis/google-api-java-client) from 2.4.1 to 2.5.0. - [Release notes](https://github.com/googleapis/google-api-java-client/releases) - [Changelog](https://github.com/googleapis/google-api-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-java-client/compare/v2.4.1...v2.5.0) --- updated-dependencies: - dependency-name: com.google.api-client:google-api-client-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 33d89ee67..09782cda6 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.4.1 + 2.5.0 pom import From 01dd6cc2f5d52ae17bba890bc3913338fac7b837 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 17:02:59 +0000 Subject: [PATCH 064/219] chore(deps): bump com.google.cloud:libraries-bom from 26.38.0 to 26.39.0 (#944) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09782cda6..c9dad6269 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.38.0 + 26.39.0 pom import From ebd86a592adb039f2f1b3f558c68ee3896f0ba7e Mon Sep 17 00:00:00 2001 From: Felix Rittler Date: Wed, 15 May 2024 21:19:43 +0200 Subject: [PATCH 065/219] fix: Make writeTimeout configurable (similar to readTimeout) (#900) * add option to configure write timeout in FirebaseOptions * improve tests and fix javadoc --------- Co-authored-by: Felix Rittler Co-authored-by: Lahiru Maramba --- .../com/google/firebase/FirebaseOptions.java | 28 +++++++++++++++++++ .../internal/FirebaseRequestInitializer.java | 3 ++ .../google/firebase/FirebaseOptionsTest.java | 12 +++++++- .../auth/FirebaseUserManagerTest.java | 2 ++ .../FirebaseRequestInitializerTest.java | 3 ++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index 8e7a29ccb..849b38b8a 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -82,6 +82,7 @@ public GoogleCredentials get() { private final HttpTransport httpTransport; private final int connectTimeout; private final int readTimeout; + private final int writeTimeout; private final JsonFactory jsonFactory; private final ThreadManager threadManager; private final FirestoreOptions firestoreOptions; @@ -112,6 +113,8 @@ private FirebaseOptions(@NonNull final FirebaseOptions.Builder builder) { this.connectTimeout = builder.connectTimeout; checkArgument(builder.readTimeout >= 0); this.readTimeout = builder.readTimeout; + checkArgument(builder.writeTimeout >= 0); + this.writeTimeout = builder.writeTimeout; this.firestoreOptions = builder.firestoreOptions; } @@ -207,6 +210,16 @@ public int getReadTimeout() { return readTimeout; } + /** + * Returns the write timeout in milliseconds, which is applied to outgoing REST calls + * made by the SDK. + * + * @return Write timeout in milliseconds. 0 indicates an infinite timeout. + */ + public int getWriteTimeout() { + return writeTimeout; + } + @NonNull ThreadManager getThreadManager() { return threadManager; @@ -260,6 +273,7 @@ public static final class Builder { private ThreadManager threadManager; private int connectTimeout; private int readTimeout; + private int writeTimeout; /** * Constructs an empty builder. @@ -289,6 +303,7 @@ public Builder(FirebaseOptions options) { threadManager = options.threadManager; connectTimeout = options.connectTimeout; readTimeout = options.readTimeout; + writeTimeout = options.writeTimeout; firestoreOptions = options.firestoreOptions; } @@ -495,6 +510,19 @@ public Builder setReadTimeout(int readTimeout) { return this; } + /** + * Sets the write timeout for outgoing HTTP (REST) calls made by the SDK. This does not affect + * the {@link com.google.firebase.database.FirebaseDatabase} and + * {@link com.google.firebase.cloud.FirestoreClient} APIs. + * + * @param writeTimeout Write timeout in milliseconds. Must not be negative. + * @return This Builder instance is returned so subsequent calls can be chained. + */ + public Builder setWriteTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + return this; + } + /** * Builds the {@link FirebaseOptions} instance from the previously set options. * diff --git a/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java b/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java index e73f93953..e12690629 100644 --- a/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java +++ b/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java @@ -60,16 +60,19 @@ private static class TimeoutInitializer implements HttpRequestInitializer { private final int connectTimeoutMillis; private final int readTimeoutMillis; + private final int writeTimeoutMillis; TimeoutInitializer(FirebaseOptions options) { this.connectTimeoutMillis = options.getConnectTimeout(); this.readTimeoutMillis = options.getReadTimeout(); + this.writeTimeoutMillis = options.getWriteTimeout(); } @Override public void initialize(HttpRequest request) { request.setConnectTimeout(connectTimeoutMillis); request.setReadTimeout(readTimeoutMillis); + request.setWriteTimeout(writeTimeoutMillis); } } } diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index c74215beb..0a4e9e81a 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -38,7 +38,7 @@ import org.junit.Test; -/** +/** * Tests for {@link FirebaseOptions}. */ public class FirebaseOptionsTest { @@ -87,6 +87,7 @@ public void createOptionsWithAllValuesSet() throws IOException { .setThreadManager(MOCK_THREAD_MANAGER) .setConnectTimeout(30000) .setReadTimeout(60000) + .setWriteTimeout(90000) .setFirestoreOptions(firestoreOptions) .build(); assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl()); @@ -97,6 +98,7 @@ public void createOptionsWithAllValuesSet() throws IOException { assertSame(MOCK_THREAD_MANAGER, firebaseOptions.getThreadManager()); assertEquals(30000, firebaseOptions.getConnectTimeout()); assertEquals(60000, firebaseOptions.getReadTimeout()); + assertEquals(90000, firebaseOptions.getWriteTimeout()); assertSame(firestoreOptions, firebaseOptions.getFirestoreOptions()); GoogleCredentials credentials = firebaseOptions.getCredentials(); @@ -209,6 +211,14 @@ public void createOptionsWithInvalidReadTimeout() { .build(); } + @Test(expected = IllegalArgumentException.class) + public void createOptionsWithInvalidWriteTimeout() { + FirebaseOptions.builder() + .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) + .setWriteTimeout(-1) + .build(); + } + @Test public void testNotEquals() throws IOException { GoogleCredentials credentials = GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index f83c5f56f..41085d181 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -979,6 +979,7 @@ public void testTimeout() throws Exception { .setHttpTransport(transport) .setConnectTimeout(30000) .setReadTimeout(60000) + .setWriteTimeout(90000) .build()); FirebaseAuth auth = FirebaseAuth.getInstance(); FirebaseUserManager userManager = auth.getUserManager(); @@ -989,6 +990,7 @@ public void testTimeout() throws Exception { HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(30000, request.getConnectTimeout()); assertEquals(60000, request.getReadTimeout()); + assertEquals(90000, request.getWriteTimeout()); } @Test diff --git a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java index fde2f918b..49805d373 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java @@ -38,6 +38,7 @@ public class FirebaseRequestInitializerTest { private static final int MAX_RETRIES = 5; private static final int CONNECT_TIMEOUT_MILLIS = 30000; private static final int READ_TIMEOUT_MILLIS = 60000; + private static final int WRITE_TIMEOUT_MILLIS = 90000; @After public void tearDown() { @@ -69,6 +70,7 @@ public void testExplicitTimeouts() throws Exception { .setCredentials(new MockGoogleCredentials("token")) .setConnectTimeout(CONNECT_TIMEOUT_MILLIS) .setReadTimeout(READ_TIMEOUT_MILLIS) + .setWriteTimeout(WRITE_TIMEOUT_MILLIS) .build()); HttpRequest request = TestUtils.createRequest(); @@ -77,6 +79,7 @@ public void testExplicitTimeouts() throws Exception { assertEquals(CONNECT_TIMEOUT_MILLIS, request.getConnectTimeout()); assertEquals(READ_TIMEOUT_MILLIS, request.getReadTimeout()); + assertEquals(WRITE_TIMEOUT_MILLIS, request.getWriteTimeout()); assertEquals("Bearer token", request.getHeaders().getAuthorization()); assertEquals(HttpRequest.DEFAULT_NUMBER_OF_RETRIES, request.getNumberOfRetries()); assertNull(request.getIOExceptionHandler()); From 2988d9c83dc1dad707195d006ac132085ed3a80b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 16 May 2024 14:29:05 +0000 Subject: [PATCH 066/219] Fix doc strings in #900 (#947) Address TW reviews in #900 --- src/main/java/com/google/firebase/FirebaseOptions.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index 849b38b8a..03f1b34a4 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -201,8 +201,7 @@ public int getConnectTimeout() { } /** - * Returns the read timeout in milliseconds, which is applied to outgoing REST calls - * made by the SDK. + * Returns the read timeout applied to outgoing REST calls in milliseconds. * * @return Read timeout in milliseconds. 0 indicates an infinite timeout. */ @@ -211,8 +210,7 @@ public int getReadTimeout() { } /** - * Returns the write timeout in milliseconds, which is applied to outgoing REST calls - * made by the SDK. + * Returns the write timeout applied to outgoing REST calls in milliseconds. * * @return Write timeout in milliseconds. 0 indicates an infinite timeout. */ From 02eda9d093eb540f1f57a28c57547424646aebd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 10:27:21 -0400 Subject: [PATCH 067/219] --- (#949) updated-dependencies: - dependency-name: com.google.api-client:google-api-client-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c9dad6269..c37d3ebc2 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.5.0 + 2.5.1 pom import From 037af4f4f0a37f91c435ddbbf22c72f879e5e9b9 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 21 May 2024 18:14:19 +0000 Subject: [PATCH 068/219] [chore] Release 9.3.0 (#948) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c37d3ebc2..c421c06fc 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.2.0 + 9.3.0 jar firebase-admin From c673c266c8a644d6f8675da8cbe1e96966d727d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:36:17 -0400 Subject: [PATCH 069/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#955) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.6.3 to 3.7.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.6.3...maven-javadoc-plugin-3.7.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c421c06fc..bd45b4dd3 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.6.3 + 3.7.0 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.6.3 + 3.7.0 attach-javadocs From 312434a8a85e602ba3e39d0cce596a23f3fa7776 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:38:27 +0000 Subject: [PATCH 070/219] chore(deps): Bump org.codehaus.mojo:exec-maven-plugin (#954) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bd45b4dd3..30b4df7ed 100644 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.codehaus.mojo exec-maven-plugin - 3.2.0 + 3.3.0 test From e728ce2d1532053b94858c886b48f6626fc8d5dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:40:44 +0000 Subject: [PATCH 071/219] chore(deps): Bump org.sonatype.plugins:nexus-staging-maven-plugin (#952) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 30b4df7ed..7f7bd423e 100644 --- a/pom.xml +++ b/pom.xml @@ -352,7 +352,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.7.0 true ossrh From 9806e3549ef2eff94801ee5ace294f591d1d6bbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:44:13 +0000 Subject: [PATCH 072/219] chore(deps): Bump netty.version from 4.1.109.Final to 4.1.110.Final (#953) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7f7bd423e..128632f95 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.109.Final + 4.1.110.Final From 9ce559bb548d92ea2b6367e699e7b9ea6b756948 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 3 Jul 2024 16:16:18 +0000 Subject: [PATCH 073/219] chore: Update integration test resources (#966) * chore: Update integration test resources * Trigger CI --- .../resources/integ-service-account.json.gpg | Bin 1734 -> 1762 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg index da547f44dff331d7c4b1e454ca1a230aef03622c..7740dccd8bdada2eecc181f75c552c00e912e5c2 100644 GIT binary patch literal 1762 zcmV<81|9i~4Fm}T0)n*iur~xoxnP%@af)-$RhZ&7)3w(?>+$-b1cw>C)uo%^A>P*=<*0iel03 za}Fsg1`ruYzno_!>aY;)OsTkc%h&&pe{$nZw5?@UjcH^%VjHu)VgwEnKQUK4HJk0= zyZ5gIrQ>xo>l)_!-wlcQad@p8gshGgv1xcg_Yjj!nW#U|n*LMBm3Wlbk?i_CZ5Ume zal+1184AaJ?*V#JRCa=MTz1K6B-nEb*&W)}8_{YN|7UTh+U!ds%WR1v?9Hbxk@ z+EogP!glp>+*yB=n_Y5O!Q;p67Vmj%K0Z&IQHoSQT$5HK`B~z1yiX&eY#WwU=~;7S z*R}F3&c9V&rDM=B?(>!Z9}?HjEfYzeD!pR<0+x@F>TU9zk7}`hv#*;%+wZyrU0HEofh5VjbOiejUW3pY z12D!+Wf#4>(VKPmD}rZ71T zja(Co^VJ`de?Q=DM7vMvigtGHs?54vGS7;%Hh%Kgpcm|%^7?KEi%S69Cr3HH;P{#2 zQKW(H3{(*{l=~Bf7Rl-tT)anXQbTFgdpURPL%zy_*wvVIb#5uC(O*IiG}XiMGUA%y z{hDOjK*5$4CjH6|s5_9ya=cleH7`i?@E_&%aD!Azwfl->_ z*G%8=hHUxvc;-w-8SZhmd?W7x5Q>Gdn&TWCqCer@&YB71xG=dZ z)$Q#4CnyByk9+}lQNhr0X%Yl^Z@x^E{?CS9l0qFDy5|%MRx3RMp&FRUbXi#z!svt^ z$1+LBRDKak{r8lR!uG7M$3y`i=mM%I6C;mOE*azp=yC&EjEO@hZAV~%vb<>vw*Tx}<@1-F%E1MJ z&Kx%>OC=ss;A2c#Xs*e!O^%Y!Dj=VPAc>{~z^9u8{@H%2A-4|8w>siL6sFHXXGAGp z6u#;-ci&~<6YWll-R0BZ(9=FJ*d=Z^1zpP&loXySLaK2LILSvW!Dc?^#B&6$r*#Z0VQL=xlo$_b_5MO*>kW6!}M4b=rO4;kC? z7>1#k9x6oenqHJ=>|?M)^;{lP0reAV^DsBc!yt{hJ=)44ze!{NMcolK=)E*z3xPIFQ}^9xIoZ8E2fWR@#G|3UOCwegz@y^#$8?WWnJ zs2>$dD-U!>e@QG8RfQO0o(=0E;V|(Wz7kVeam0oZT7r|6xiFJ zNd8hqdzqJ0-`fL-a@okskU*`zWMi=%n1PPxq!&m!cL|J#l~QZ}A3JebK?T zy$sIEE!|IkVGk9;6HzHA)9Y-IXTCA)$QGOVqdT1OhM%`yYT53qvYtc=4KjLTfqCH9 zgC*H0Jk^%P@UH!391-ApSu$Z{4D#3;W{vq*kO zsL+qpghA`Jm|4{gJrTOb%(>>3vR5{Am*n3SBI9H-+h-WW$}kgTg9MyYUUi*K+#nml Ero6&wRR910 literal 1734 zcmV;%208hR4Fm}T0_WqJMLyD96#vrd0kSseMv-_5+A>|dXkh)Lxg1WlZ_;HhFTh#x zDXB$vd955)y_i6fJ)Oj)aBCs+sb^rD8ckG^9BEnrFn(oz#yfzS@$+cn3lx!+B3v>8 z+oZYA$+d_~HR_?8Y|47V&47`mfrj&cS28OWOy@;W6RABVdPnTBDiQppT7W_J!vZ~k zJU+n4wlc$u!0!vj)k)conSKAdiLj5M4~3gTnMvFiA3OAaxG9$-Fi4Hz8zrK}UW+wP zy2KkBMs*nJt{;S3op3>^&flbtsElr~I!hsD^U2aEDCp+ScE}*!0)iJIYxI~0rz0!! z$*UJ{b*NbH9yB>mQVu(OxgZzVjskCoA1LLO)@Ob*I`%xmx3pY(UfU8<8kU$joj5L4 zATtgXL$nQ}etB%PN4yHDL~%HN5EDvD5vvXO-mjxu`Jw^zi}3M^AJM(=3}3DOf8t`1 zj@}P~D3y#8&Z~?x@1we($j}oT5e)y|907FiLk@uC)}}$TRV@Z^%|3?~AHf{N=JdS& zmAzu?{0*volC=e78`s^b_7LW?R+;1;+(&V(( zT*de9#KZNX^&6OE$fSpA*AbRQ@qfp9pz}Jz&>w6bXOw_#xf2&v-L&Phl|oHWm@D3} zOys2V$RMQlp$0{v*s34|sV=iP;x;zP=5RpwWJqnMP&QI7t)^63FDPCOen|MjFf6Fy zbH8(4!{wkLz8MP^38`HGosmm`E%%?rxE~2g)rNC_O(^3S*C5=3vYzG?3+y!oJ{s{1 zzUNA>f{FX&_8Zr~`3Aw+iI_(l<`oeGJGikkiq2sFOWR;=B!0NQRSX2%#^zb?1Vrn0 zUAk&@PC~v?RWOgIT?*LH&uK#GKtFXuFkP#zy6ByVhMXCsN=h$QNcx6Fab!8QW6Ur& zu}^KvHWAtV9#3GkF0lWTE~;FldqZH>&o2Z%KX~iA^p+fyiGvth=T-)<@CAc* z?GOwo%qlyt!74iU48M9XWmg)7k`fK)b}RNsiVo+UFE1g{y{tK^E0QF7xiW`>{iE_L z2e)`aCjrF4vaJ(AvkPqelNNjHn&uE@P7P07O+*CceK`8`Ej^O>;!(i7HaCaidZ~kj z$!=OMn@HL1CK6=ew?$zQygRGVm+>k2DR7{CKke27evSnGj z1DV~gv?jynB1yH$8kV3HDk^T{2PYCrSXN1{hh09 z;l&(GhZ7|l=@6E5%7=&;~1HUa3Edum39CeKllCDf5wr&+x@&+?!$Me2rKSZQ|DnUwYV~xd6kbA z^fr&FN-_FU?HQ=YD~>e6BR*{U((QLQmN58HK)d%N*5aFTL7;s!iBET0tq(c}0dVe8m%YTz{Xp6^}&9rC_ zFW>egRI>>@J|qb&;qSrsBP~vN2r{|}xny7Ft7$IqnF5kN{Ynzk**xBl6FNa!ASKZr zjkC^DnaK9M?q75@{PckJ6UMReK^Nr?iNe=Y6Rz(Imio@B{ax^h!#Hx-I^eU|&f@Sk zeh`^4eBkBo*Y~t$e?2{Lb0)LXxo?T=3jk#6n!+oCl3nSeNpa@uI5OjP zK#5K-d&uK-Mex4OqRTzG{P7!FFn+a From db472394f73275e7bb60faa091c4360181a022ac Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 6 Aug 2024 23:40:21 +0000 Subject: [PATCH 074/219] chore: Update GHA resources (#970) --- .github/resources/firebase.asc.gpg | Bin 5213 -> 4078 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg index dae950b79907fe1755832d9d37ad34cef5e2e284..feb690edb55d475e520c91829b1bddf74e5210a3 100644 GIT binary patch literal 4078 zcmV@KM2ENfd` znc#W*a3Ooqd*(W{g1h_a8@BIbn7Nt>^*8M7f?&enK2bc&q|G~nl#e|f<2iTRItr2~ z<8LWj4`IS0eBc)Y)1(D8Qnh*R5a*?g zt;VUB`_4$)G30ADeXbW9j_^MOT>5b6LVbl;EQ6{8%}k`3TzszD*fk!l?!-5&Twqgm ziC_HBh84?sC2A)O@^nnt7X)m6H-#dVQm$bp#vd~TNg@pm;`L=y4cb^-cIFte9V3e?Y2`s|I&Xw-=Xhff??~-R>{K%)84+;-8g|Ju0*l&04TLb4b-vPvL8lrFk~VW zs>D<1-tYm>mI9$KEo#j_i9{%EOqaq^H6i(>GU#}5aAEh`B8A*_F%EbEbVld04aA4g z6}J6Ek+CfOJc!6N;d;^6(~Q~-jIsN>3#ER;5E{fK9v}qQqf- zoJ!LThr^Y{843w;o|SUab2q8Hbi9WC6_1)$xJW4Mhq%YOwSW4=6q9ASm=7VAzhcr3 z1w)OTjuA;1PaWv`FdrOZ^Hs-28s_c`@ua&Ah$ zi${nW!Vk$Z7x<4l(!HbY6)KyS|gW`KGoitXa8m%#-k>|5@Y8mkoC{c8~TKdIRL7r(AdMf_+~wYXRQxht!( zD{D zL{JkyB6SRl*~G5Ty$PDu6TG7?hqbUDx3$d4=#98gb&7iabITcOFE=}Ss#bill^)Tj z+gR2=zxAs9GAh)Ql~}yg+YX=={Wil5%>gic6!6^bRzp#?K9nZWDI$JE(>Lz$YQZb; z^v`lltn%;Sj5-aNQG;43ujMC&T2;yThD&)`FiiQpV}|2(>GNXh2d*Cx1t#F$h=2pSCGfKlunKi$YK57L56Dtr=fRRtj-qI2kNpi}2Q^ zB154`675|)zdKJy;EBb+_Jo$RnU3UjD^3;H=Ethqt$Y6s8kpNW>j@>XT#InnAQ#Tv zq{3?io7fp+&A;QxCS}u~JcS*SZJt8Dmf#lj3s{rUBnkE?RwO5YuyG{{)3;Ij? zFHspS+eq|NtP3W8jn?`e`&!|h@uZ6m?D>6?9aYg0R+(~i0SjaI5?+{kOZKNHs8-*X z*@;2nopIv7&asv{fQEG!01RN5w!HI@g;u=C{_hsZ!Cldo%6Tl>LNkfbCu;!ozTH4L zXyu~Ea2~aULuul^v^0j*&VQ>0ApoG+Ocuu#P@&2NHKYdAP5SEomQ)(H7+x_2VRU8+ zjtU`Zra4$+*g$n@{8M$Mf@gk_545HMU-o`~-JF6SiL02m zd)2{k)qETfW-|8%feRGm6eo+(qV?=e5sqXkqnS}W+8>bmM0I#dp!PbK#rXX;r!Fop z?osyTL~1kCR1A<L_FImJFXKI zD}Izrm6!-7v8V*|x2dob34a`$X$du_B@WeeA&$47OfPB{*R*avqx9|adC~JVrbbcX zKQ<~HEIAAGd8AS4op4DO40s4*}tFidP8!l+~>c%#?t$br$ zlUw(xg?Vyq>|i{tc7|i%s8PXDeq}dJ+X^Hs+fCg-0Sq25_!!3{H5vn3Y~c04T@-Wm z{2;spuy=7kYxC&EL~IW0C$y&ViMQM7Hib|o@&=kyVDcjs72ZW9u#vR8jglzn!Tr zUVdnmKBskyd1hDdMbOWcZQ1;7+{xJD_&6c*&nZ9#m2!GX<(-^`<-p5gc`n35Trm=R z0qXbS;F4TWjruUJc`>la!PE0h=05=4=zY+lxer^l)aPkzF+cYyOo!(Zd+-s^=5?kJ zbmUI|xUdW&`s$(lVW7qSs*YT_X5Xgl2KzN)ijnrEIHPvO`@;>hYGRF>X6V(71P9eh#z8xW&N6 z&D_j?e_LysY$B}2!}8{m2j%>aC}mK{^80`_P-I<>gu{y&JlrG?SQI3d=_H_#9l`yk zSr6;J9t#BIcWMC|o{9b(=D*O+kkg-%DbT|s+(<1ldE+LsMW8q>)k8&BpANs1gdeV5 zvd;hi+(&s`BQ?iu$n{MkP?_$1?QsT~`mpl^u^$sOe5MoctjNY6MYsqIYLQeR%PG+u zzZ<_}b%Tb>WP&oC@H<+SL-&_}^x>A{%gGsdWHE8vZW6O+J69RRC97bPiuO{_1av>+ zxy8JkX!S%-8x^?9T0VE8u9vDo4b{h&%0pVHF(+6I1g2AdU|uB{Vhry}j0S3#s>mo2 zbh>&S!<2$uiD*{;aHw<}=|%Ht3vi+v0521EGkJhw>ow?sIt3jPO86zHt$p~WJbt10 ziM#lf{DnH$!5SAfC?0amV>nBO;zgE@(xA7ZPKbDB`7F+wL(Nj|ubUx8f<|MV^!*l< zZ465}A%c3VXdFyL1`DCwHUl6djAmD~0_D_Y8Li!ZD&C7!TlE22?>X|Z>9V?dz;q%L z4jwEMo_GHOrVAex0Dool7CwI2rSP$-K6m->U z5$TGTK0S2_9Rq6q^_t`poDSrkUzMvvpv|@|7uX!C7W)k{^k?Rd8TPl}oXf>?RB=MV zC-bGECQ_xMxCEyxTsNWypXXwvmQbWWMC?e8x9J<^hxK>%B@kSBjOEPsPSx2s$&jzw zUmAwjBVECS8p`cl&~ch1-kD+;A+-N8r2T{t%oXns?OaCP{4a^RlyKB~XR_yGXST0r z+q^%bJS0W)C<6XAo{P3XtS_OnvUiOya6#tOEg6Ok^uP?r5~%VjI{WcJ)c-bqJ1M`4VkU*Z&S=hs13i$!t8MK zxhR1Or*Dc+w1Mxk6A$X$zk&--%wXXj4ZvTQ<+qF{a~Az9#-IRyw+bFeuMOqqyzNb9e0jgD9K%dSDj*zb?{vRYnL|$) zSF?pTBkqMIYho$kKK!mAN&nEI_gTf7hO|cEnq>#KsO?p%t3X{D((akW-ghN2JhtMQ z3R-6vnBTd&=Ax8h-S(cCrqVb?)kkZ_Nl?l6JyEW zDuxY(4xmK}UEs=<^4pp29v`<&_Q4 zvr9p-TjdRBaUj(A=EV3D7%WS$HU7Wl?$pAz4J{n7oqET#Wj!ad$s)GxAm6lwu&m@jqXF2t7fY&)M0JbQAeBLHp=odW_)Iz3(<{dUbrYik5#>A^QO zj~xc02=e9?)Z~_2t?!_k`(ZmsX+Q6fmE}U@afYv(ORqUl06go~;U`0Kz+w$1Q(;!` zXh^_0zX&PO9lXa}GeDj>x|eC`K`&7H=qWI!sxLC5j!_5c6? literal 5213 zcmV-j6r$^l4Fm}T0y~cD1mMJ_9{NSJ|6_^aOlzDF;9a=bJ=4>+~FpqQRBCwHSU;vuJq+^UYpVdggTmZa@nZ zh%<#5tD=T>i3_X%SbV<2CTS7D4c#%aWv)d#b*tw~8+%O{skDSn|13iD2v)_1xzdfx zd|N-H=t9%Fl}2giyw5{Ko-Yi%h2^JwFp>_YEU}vRh3*8E<>KjRhm_>N0-Et3d9jru zcglr?n~c_YSsz6F^M5CZ6c^N|c={`W@y04~XVu+29IoBZf0&sBDqggsVeMcuCFjyRczh;{iahd*t}*fNh7Y&8{hAouJ*d`L zLWIR{NJw-U5mx&0MsxLXLFTW`mXC%Yb($@IP4aQam*rc}o$jj){;`BWb%*GXoGrmbShPt=lzBr%B!Z8QZ~0WaMV}5 z)Ae`!7C-WsW+n~8cqYdF>dEhUtr6di!U zg7DWt`tl}u%-FV28=Ut>XZJ|DyQ!&3#$7xT*App?cawN9el^octlJhGCwb657Kg`# z%Dda_0-j;vIiqfx{j?n5`RLiMNYN-huKS2B$jqTJTzH>of45!+F1`Kces;fvdQgcM z0*YsDI+0s}Q?Ww*c?U7qrYZd&b{{Mj=j>`J$x9@Eoz_Dn=KSALFFqi75s|JdIy^fx7vxN&M2He#y3zgZvkp@I1BuSMQIV5Pyq z+pH4*kP2>ps;;66BZUFe5puUt5>AjvdI;hy&~{^b>m2j0Pm}=15s7u}qWeSvAN_xO zFTK-J=GM4upv;@lMs%7e72iDQiDFEMJaNYnUZ=T^^WsxFu!l_UD5lOU;tXtlzL+(~HMS;Gx2aLeKTdj%Gx;V*D=(3db+ zwWSCh?d(vsBKxj?e1qQt>2JWs;I@5A7#gCKoLx>9V=&A!ZS;~}+&z!IkJaVGAQ%Qg zb^L8PX7bvMx(&hjf1eQwM?yS+@&p4Q0_xsFo&OJ>4I1d;LIF8NcJVMdB~U{Ffw^MQXx z&9vf}0n)!`L>n9ML{yOYR{F|iiLdsROq1h+1{}PLJa2>Q-8#}eEbO_$OQfNs$;x3~ zR02wC;HAo3NmHasI7207%XyLYV&Z72U|2(MEwO0Uh&eNGr8%v&p`X%1DaYxgl}$rz z@v7eUU1St}cN9s_o%N@VgGGt57^^1vtF|0eoNSDdwTzP?)?68|xeP%QqW=l1!|9^uwEf|YAhU4T{bn8+~W$5>maxkg4Hd6%FsoN7c zO~I^o*L2nTZR&zNl07Oi)y{oN^DtcvK(SOI*Y0>Ybuar6xrG(XK^dfK`&C4MJ-e=K zZnKxHf%qwe;50O8;3k)4bdoBjyTAqs(!tnp$Buf68tvWUlz7z$6xk$i)kD zX=0YAOOh$u#R6V$%F?*l&$EHHVA{gVEh(NNv|bKo^$yxT4}#pL0Blp9spkEst&Z!g zqbMUyFXg+eZjeT+^RTaV&Jm)hd21Y%AaW3^Lqblg)2sER6*#gei94~!<(M{b_3{HF zwd3`)aIxZMI3J&c#~0b?w01xqb{S)|QK83#L%(IKo!QdckrvC!zIHb6tu1CQIt%XL ziK;-F57I`VaR3d{o^SW0z`Tlg6r|7(u?SW;R-=R$T0cuCo_SIyidv+e-Z zlm<`xPuT1mGQbfoUQC!VzP^{#%8U?tCE?zvh{b+;_VNmb%-x*u{R5Qtc9U?x7fC|;Hfrw7oO#%MOJ--mnj1<&fOU)~#i76#MW=V27AZO-$K(LeD zETl~4j=1ql&j~rVzN$L)%@D+U1M2)Dj{*{6)I}&UPGq0OE^K zpXR(!Z|F2*D(P&67eq-9X5&(LiN1Y}@htX#z}jOO8|N9rLrve76IAB00?FUVz0e(Y zx;0bf^@>O=lvjkR#9q*1!B%*NO9^PH$S)rn)%%g+1nKM?F!pRRxBRQ0VeCplGS7_| zsh|S;`1XAL_9f*Sss)a)YzWY^Z_o(#^%mc2^A2fbC~Vtls4maI_>Dr{FFRZ0&Pqq) z`wdZhsh<38I16GIm{0ZTEr__!fP+3G{$(2JR{mhg@V4G?kX>M zQ~JTq;1ey%npIir>qza~5oHsK(oV>Ht+t}HZ0@=_D?KHEST}Je-cZ-AI~>5&{t21a zn42zwbYG7}h>#bw608CQV-Q-v1!o~0b|hj9_R@_qhLF%pA$G8kc^ zC_tXksu8YLCz4CM;(D3%-ryzJ7)na?G`Ml(Q`3Ns`!Yvg9dUfe-LRSy9?uho8Y2r# zM+W8ftj=T-A6`83-Xog@nS1Wz#5}I^TmQ1c&F4zGWb80K5TFXTsEZ<?|9v;S*RR1OTUf9YJcxZ zCVh$uA#0s=$pB?Dz3<|t4T;3!kdqw9;y%G-dIkz~W+??tSn046hCZtL!2l`r%7nu~X-p-g zktkXLj~Qu04H$CKb!C);=}pCVe>6jf&Xof@Or$Z7(Kf?UYgVrljDij~n!5pdrAX*h zmag^pefSf4E_boQ1JV~T-BZt6QcNhOMdy7;2tEX@P@j19q&SzqEZQC|Eug08?UUjl zSLZzcX*LTzr(@20JJNi%7)Pt+HFLev{Jc;GkdI0RgeFOpQ41(4`R$v0~7Iy_KR%wT#&H(nTV`|6XCIt6*=d_M&7iVrtPCYl6I$ z9Xs6+BtcYY)vh;lIxle`)@Phfw@xRvS#Z?(0~b)V5@nYemY-UWvYxmX?XD_-zm#Q* zq~Aup`}dRl?Jndj|E|r_2QH(1@9GvUh=&agz{ z`BFGi6qXUV=jAw_E2ioD$`41a<7fJ*4Im*iZLve}sTPEq+^Hidp!awyGOS~tL>8rV zwiPNSKve@m-*Fd7I(HOE6r!Dk=Cyr#q*X;A+%W9`_0AI~oE=fOUGk%~%+@)L*A^z) zD_$-y@5AMaYx_AH#6VQY?C98DY4_$FWfuaA7J@a6DnsBZ*J=D%x-Rtx+|gQAUx0|5 z9tVWKXZ>u-{_&b6w7vKIK~p>czIL@JkKw6Z6o}66Xw~IAKx7{_B$4nMFB51}N~-$3 zs`8z!S_12b>iz?Plj|e~1LinXR|$vjG-G-CIvJql&leGNU09xUm?v3@St*^kOJM6a z(4<3o_%=)S0@n@~wkwOE9j5+x(UQT$6p6WlLnC>19D6LpujoCJ|0sY>nK|e3hb+>A zExOpMWTv00f7|i*)loysK-wxGfEB>ooR(4ojRgc=yGev{W0vNW`fAwDh%rsXv>HTR zWOCt;b;`sZcvh+#WB@ue2$3I*uAfZh;DaSOkR zX+V4KL+PwD^t6m5} z$`%AFpYhO1>Ta-;>Sd6lxwR96;*2EyJH7!1vVA`N(a7?{cNKBSXj>E>=gU6G(xPI#O>NY`-DvZK@}Q`%7k7 zTJ28;wOmL6mfQKFM(M%0O>wr;kI?ZjI+i`b6V?w;MBd00#Tp1YEL|TUqdOf=wwzcG zUd6{L_b&v{yZTZn59HISeoLE4h|1KN!HYI!(Kdp!v0%XIH3ve-gKX)Y^#D4!uk*KE zxY8Q2VoNnL6^^z;ADGg|()E-#?PFSI{ATHo(^BZ9T<~|F%WZWr&|JVm7~?I?_zG1 z20U2JtZuP{KO?}g(Wtp4GrUVNBAZ_KA<(*`0ZFUxIEwkfsSxO0jFYqR$ar+WvNh+3 z#?`SS1=E4Y@Lc`3`^jkg*>%cv`&7%o+f3HpZXIqZnE3{)6;!mFr4`Ww`{2Rc?QxFb zYVp_h)YqFpcd;0iAHE=ex$EYy0eUIpc1X(5wGi17HWTpM$~gm#k-y*2jdSyJSl`de$Jd^wjLMoL$Ax@B5lGf> z4FxvLE9j7m%wTH=^1STMjE`t6XSeW%0?}voD8f2ePtYkM(d1JeS~V~<6jfqvzeieJJ3oPP|?Ss_L#q1s z!upQ5m~;!NlZ3WqJ&nAA9iz5q6o<$yy}nO;WU%go8bT@5`if&rsX%C) XrIgL-1=1;}YjbYhxg363z=nJD3 Date: Tue, 6 Aug 2024 21:21:50 -0400 Subject: [PATCH 075/219] Migrate from the deprecated Charsets constants (in Guava) to the StandardCharsets constants (in the JDK). (#971) --- .../com/google/firebase/database/utilities/Utilities.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/database/utilities/Utilities.java b/src/main/java/com/google/firebase/database/utilities/Utilities.java index 5a3c64822..6181071ec 100644 --- a/src/main/java/com/google/firebase/database/utilities/Utilities.java +++ b/src/main/java/com/google/firebase/database/utilities/Utilities.java @@ -19,7 +19,6 @@ import com.google.api.core.ApiFuture; import com.google.api.core.SettableApiFuture; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; import com.google.common.net.UrlEscapers; @@ -32,6 +31,7 @@ import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -108,8 +108,8 @@ static Map getQueryParamsMap(String queryString) for (String paramPair : paramPairs) { String[] pairParts = paramPair.split("="); // both the first and second part will be encoded now, we must decode them - String decodedKey = URLDecoder.decode(pairParts[0], Charsets.UTF_8.name()); - String decodedValue = URLDecoder.decode(pairParts[1], Charsets.UTF_8.name()); + String decodedKey = URLDecoder.decode(pairParts[0], StandardCharsets.UTF_8.name()); + String decodedValue = URLDecoder.decode(pairParts[1], StandardCharsets.UTF_8.name()); String runningValue = paramsMap.get(decodedKey); if (Strings.isNullOrEmpty(runningValue)) { runningValue = decodedValue; From 90b0cd60f92fa35776223f21e1ca6982a7739c4f Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:50:42 -0400 Subject: [PATCH 076/219] chore: Remove tests for deprecated FCM apis (#978) * chore: Remove tests for deprecated FCM apis * Trigger Integration Tests * Remove remaining tests for deprecated FCM apis --- .../FirebaseMessagingClientImplTest.java | 260 ----------------- .../messaging/FirebaseMessagingIT.java | 97 ------- .../messaging/FirebaseMessagingTest.java | 272 ------------------ 3 files changed, 629 deletions(-) diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 17848cc65..3180aea01 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -17,7 +17,6 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -29,7 +28,6 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.HttpTransport; @@ -73,16 +71,9 @@ public class FirebaseMessagingClientImplTest { private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; - private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( - "fcm_batch_success.txt"); - - private static final String MOCK_BATCH_FAILURE_RESPONSE = TestUtils.loadResource( - "fcm_batch_failure.txt"); - private static final Message EMPTY_MESSAGE = Message.builder() .setTopic("test-topic") .build(); - private static final List MESSAGE_LIST = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE); private static final boolean DRY_RUN_ENABLED = true; private static final boolean DRY_RUN_DISABLED = false; @@ -320,187 +311,6 @@ public void testSendErrorWithDetailsAndNoCode() { } } - @Test - public void testSendAll() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient client = initMessagingClientForBatchRequests( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - - BatchResponse responses = client.sendAll(MESSAGE_LIST, false); - - assertBatchResponse(responses, interceptor, 2, 0); - } - - @Test - public void testSendAllDryRun() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient client = initMessagingClientForBatchRequests( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - - BatchResponse responses = client.sendAll(MESSAGE_LIST, true); - - assertBatchResponse(responses, interceptor, 2, 0); - } - - @Test - public void testRequestInitializerAppliedToBatchRequests() throws Exception { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(getBatchResponse(MOCK_BATCH_SUCCESS_RESPONSE)) - .build(); - HttpRequestInitializer initializer = new HttpRequestInitializer() { - @Override - public void initialize(HttpRequest httpRequest) { - httpRequest.getHeaders().set("x-custom-header", "test-value"); - } - }; - FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(transport.createRequestFactory(initializer)) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) - .setResponseInterceptor(interceptor) - .build(); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - } finally { - HttpRequest request = interceptor.getLastRequest(); - assertEquals("test-value", request.getHeaders().get("x-custom-header")); - } - } - - @Test - public void testSendAllFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient client = initMessagingClientForBatchRequests( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE, EMPTY_MESSAGE); - - BatchResponse responses = client.sendAll(messages, DRY_RUN_DISABLED); - - assertBatchResponse(responses, interceptor, 1, 2); - } - - @Test - public void testSendAllHttpError() { - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent("{}"); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, - "Unexpected HTTP response with status: " + code + "\n{}"); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllTransportError() { - FirebaseMessagingClient client = initClientWithFaultyTransport(); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); - assertEquals( - "Unknown error while making a remote service call: transport error", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); - assertNull(error.getHttpResponse()); - assertNull(error.getMessagingErrorCode()); - } - } - - @Test - public void testSendAllErrorWithEmptyResponse() { - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setZeroContent(); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnull"); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithDetails() { - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithCanonicalCode() { - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithFcmError() { - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - MessagingErrorCode.UNREGISTERED); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithoutMessage() { - final String responseBody = "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"; - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent(responseBody); - - try { - client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); - fail("No error thrown for HTTP error"); - } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - MessagingErrorCode.UNREGISTERED, - "Unexpected HTTP response with status: " + code + "\n" + responseBody); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } @Test(expected = IllegalArgumentException.class) public void testBuilderNullProjectId() { @@ -563,18 +373,6 @@ private FirebaseMessagingClientImpl initMessagingClient( .build(); } - private FirebaseMessagingClientImpl initMessagingClientForBatchRequests( - String responsePayload, TestResponseInterceptor interceptor) { - MockLowLevelHttpResponse httpResponse = getBatchResponse(responsePayload); - return initMessagingClient(httpResponse, interceptor); - } - - private MockLowLevelHttpResponse getBatchResponse(String responsePayload) { - return new MockLowLevelHttpResponse() - .setContentType("multipart/mixed; boundary=test_boundary") - .setContent(responsePayload); - } - private FirebaseMessagingClientImpl initClientWithFaultyTransport() { HttpTransport transport = TestUtils.createFaultyHttpTransport(); return FirebaseMessagingClientImpl.builder() @@ -603,64 +401,6 @@ private void checkRequest( assertEquals(expected, parsed); } - private void assertBatchResponse( - BatchResponse batchResponse, TestResponseInterceptor interceptor, - int successCount, int failureCount) throws IOException { - - assertEquals(successCount, batchResponse.getSuccessCount()); - assertEquals(failureCount, batchResponse.getFailureCount()); - - List responses = batchResponse.getResponses(); - assertEquals(successCount + failureCount, responses.size()); - for (int i = 0; i < successCount; i++) { - SendResponse sendResponse = responses.get(i); - assertTrue(sendResponse.isSuccessful()); - assertEquals("projects/test-project/messages/" + (i + 1), sendResponse.getMessageId()); - assertNull(sendResponse.getException()); - } - - for (int i = successCount; i < failureCount; i++) { - SendResponse sendResponse = responses.get(i); - assertFalse(sendResponse.isSuccessful()); - assertNull(sendResponse.getMessageId()); - - FirebaseMessagingException exception = sendResponse.getException(); - assertNotNull(exception); - assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); - assertNull(exception.getCause()); - assertNull(exception.getHttpResponse()); - assertEquals(MessagingErrorCode.INVALID_ARGUMENT, exception.getMessagingErrorCode()); - } - - checkBatchRequestHeader(interceptor.getLastRequest()); - checkBatchRequest(interceptor.getLastRequest(), successCount + failureCount); - } - - private void checkBatchRequestHeader(HttpRequest request) { - assertEquals("POST", request.getRequestMethod()); - assertEquals("https://fcm.googleapis.com/batch", request.getUrl().toString()); - } - - private void checkBatchRequest(HttpRequest request, int expectedParts) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - String[] lines = out.toString().split("\n"); - assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); - assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); - assertEquals(expectedParts, countLinesWithPrefix( - lines, "x-firebase-client: fire-admin-java/" + SdkUtils.getVersion())); - } - - private int countLinesWithPrefix(String[] lines, String prefix) { - int matchCount = 0; - for (String line : lines) { - if (line.trim().startsWith(prefix)) { - matchCount++; - } - } - return matchCount; - } - private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { return FirebaseMessagingClientImpl.builder() .setProjectId("test-project") diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index bebae6e4d..d9375781c 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -193,103 +193,6 @@ public void testSendEachForMulticast() throws Exception { } } - @Test - public void testSendAll() throws Exception { - List messages = new ArrayList<>(); - messages.add( - Message.builder() - .setNotification(Notification.builder() - .setTitle("Title") - .setBody("Body") - .build()) - .setTopic("foo-bar") - .build()); - messages.add( - Message.builder() - .setNotification(Notification.builder() - .setTitle("Title") - .setBody("Body") - .build()) - .setTopic("foo-bar") - .build()); - messages.add( - Message.builder() - .setNotification(Notification.builder() - .setTitle("Title") - .setBody("Body") - .build()) - .setToken("not-a-token") - .build()); - - BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages, true); - - assertEquals(2, response.getSuccessCount()); - assertEquals(1, response.getFailureCount()); - - List responses = response.getResponses(); - assertEquals(3, responses.size()); - assertTrue(responses.get(0).isSuccessful()); - String id = responses.get(0).getMessageId(); - assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); - - assertTrue(responses.get(1).isSuccessful()); - id = responses.get(1).getMessageId(); - assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); - - assertFalse(responses.get(2).isSuccessful()); - assertNull(responses.get(2).getMessageId()); - FirebaseMessagingException exception = responses.get(2).getException(); - assertNotNull(exception); - assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); - } - - @Test - public void testSendFiveHundred() throws Exception { - List messages = new ArrayList<>(); - for (int i = 0; i < 500; i++) { - messages.add(Message.builder().setTopic("foo-bar-" + (i % 10)).build()); - } - - BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages, true); - - assertEquals(500, response.getResponses().size()); - assertEquals(500, response.getSuccessCount()); - assertEquals(0, response.getFailureCount()); - for (SendResponse sendResponse : response.getResponses()) { - if (!sendResponse.isSuccessful()) { - sendResponse.getException().printStackTrace(); - } - assertTrue(sendResponse.isSuccessful()); - String id = sendResponse.getMessageId(); - assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); - assertNull(sendResponse.getException()); - } - } - - @Test - public void testSendMulticast() throws Exception { - MulticastMessage multicastMessage = MulticastMessage.builder() - .setNotification(Notification.builder() - .setTitle("Title") - .setBody("Body") - .build()) - .addToken("not-a-token") - .addToken("also-not-a-token") - .build(); - - BatchResponse response = FirebaseMessaging.getInstance().sendMulticast( - multicastMessage, true); - - assertEquals(0, response.getSuccessCount()); - assertEquals(2, response.getFailureCount()); - assertEquals(2, response.getResponses().size()); - for (SendResponse sendResponse : response.getResponses()) { - assertFalse(sendResponse.isSuccessful()); - assertNull(sendResponse.getMessageId()); - assertNotNull(sendResponse.getException()); - } - } - @Test public void testSubscribe() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index dc07ce002..6d61f51de 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -545,260 +545,6 @@ public void testSendEachForMulticastAsyncFailure() throws Exception { assertFalse(client.isLastDryRun); } - @Test - public void testSendAllWithNull() throws FirebaseMessagingException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - try { - messaging.sendAll(null); - fail("No error thrown for null message list"); - } catch (NullPointerException expected) { - // expected - } - - assertNull(client.lastBatch); - } - - @Test - public void testSendAllWithEmptyList() throws FirebaseMessagingException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - try { - messaging.sendAll(ImmutableList.of()); - fail("No error thrown for empty message list"); - } catch (IllegalArgumentException expected) { - // expected - } - - assertNull(client.lastBatch); - } - - @Test - public void testSendAllWithTooManyMessages() throws FirebaseMessagingException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList.Builder listBuilder = ImmutableList.builder(); - for (int i = 0; i < 501; i++) { - listBuilder.add(Message.builder().setTopic("topic").build()); - } - - try { - messaging.sendAll(listBuilder.build(), false); - fail("No error thrown for too many messages in the list"); - } catch (IllegalArgumentException expected) { - // expected - } - - assertNull(client.lastBatch); - } - - @Test - public void testSendAll() throws FirebaseMessagingException { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - - BatchResponse response = messaging.sendAll(messages); - - assertSame(batchResponse, response); - assertSame(messages, client.lastBatch); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendAllDryRun() throws FirebaseMessagingException { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - - BatchResponse response = messaging.sendAll(messages, true); - - assertSame(batchResponse, response); - assertSame(messages, client.lastBatch); - assertTrue(client.isLastDryRun); - } - - @Test - public void testSendAllFailure() { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - - try { - messaging.sendAll(messages); - } catch (FirebaseMessagingException e) { - assertSame(TEST_EXCEPTION, e); - } - - assertSame(messages, client.lastBatch); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendAllAsync() throws Exception { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - - BatchResponse response = messaging.sendAllAsync(messages).get(); - - assertSame(batchResponse, response); - assertSame(messages, client.lastBatch); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendAllAsyncDryRun() throws Exception { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - - BatchResponse response = messaging.sendAllAsync(messages, true).get(); - - assertSame(batchResponse, response); - assertSame(messages, client.lastBatch); - assertTrue(client.isLastDryRun); - } - - @Test - public void testSendAllAsyncFailure() throws InterruptedException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - - try { - messaging.sendAllAsync(messages).get(); - } catch (ExecutionException e) { - assertSame(TEST_EXCEPTION, e.getCause()); - } - - assertSame(messages, client.lastBatch); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendMulticastWithNull() throws FirebaseMessagingException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - try { - messaging.sendMulticast(null); - fail("No error thrown for null multicast message"); - } catch (NullPointerException expected) { - // expected - } - - assertNull(client.lastBatch); - } - - @Test - public void testSendMulticast() throws FirebaseMessagingException { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - BatchResponse response = messaging.sendMulticast(TEST_MULTICAST_MESSAGE); - - assertSame(batchResponse, response); - assertEquals(2, client.lastBatch.size()); - assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); - assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendMulticastDryRun() throws FirebaseMessagingException { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - BatchResponse response = messaging.sendMulticast(TEST_MULTICAST_MESSAGE, true); - - assertSame(batchResponse, response); - assertEquals(2, client.lastBatch.size()); - assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); - assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); - assertTrue(client.isLastDryRun); - } - - @Test - public void testSendMulticastFailure() { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - try { - messaging.sendMulticast(TEST_MULTICAST_MESSAGE); - } catch (FirebaseMessagingException e) { - assertSame(TEST_EXCEPTION, e); - } - - assertEquals(2, client.lastBatch.size()); - assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); - assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendMulticastAsync() throws Exception { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - BatchResponse response = messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE).get(); - - assertSame(batchResponse, response); - assertEquals(2, client.lastBatch.size()); - assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); - assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); - assertFalse(client.isLastDryRun); - } - - @Test - public void testSendMulticastAsyncDryRun() throws Exception { - BatchResponse batchResponse = getBatchResponse("test"); - MockFirebaseMessagingClient client = MockFirebaseMessagingClient - .fromBatchResponse(batchResponse); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - BatchResponse response = messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE, true).get(); - - assertSame(batchResponse, response); - assertEquals(2, client.lastBatch.size()); - assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); - assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); - assertTrue(client.isLastDryRun); - } - - @Test - public void testSendMulticastAsyncFailure() throws InterruptedException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - - try { - messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE).get(); - } catch (ExecutionException e) { - assertSame(TEST_EXCEPTION, e.getCause()); - } - - assertEquals(2, client.lastBatch.size()); - assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); - assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); - assertFalse(client.isLastDryRun); - } - @Test public void testInvalidSubscribe() throws FirebaseMessagingException { MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); @@ -947,14 +693,6 @@ private FirebaseMessaging getMessagingForTopicManagement( .build(); } - private BatchResponse getBatchResponse(String ...messageIds) { - ImmutableList.Builder listBuilder = ImmutableList.builder(); - for (String messageId : messageIds) { - listBuilder.add(SendResponse.fromMessageId(messageId)); - } - return new BatchResponseImpl(listBuilder.build()); - } - private static class MockFirebaseMessagingClient implements FirebaseMessagingClient { private String messageId; @@ -962,7 +700,6 @@ private static class MockFirebaseMessagingClient implements FirebaseMessagingCli private FirebaseMessagingException exception; private Message lastMessage; - private List lastBatch; private boolean isLastDryRun; private ImmutableMap messageMap; @@ -987,10 +724,6 @@ static MockFirebaseMessagingClient fromMessageMap(Map mes return new MockFirebaseMessagingClient(messageMap, null); } - static MockFirebaseMessagingClient fromBatchResponse(BatchResponse batchResponse) { - return new MockFirebaseMessagingClient(null, batchResponse, null); - } - static MockFirebaseMessagingClient fromException(FirebaseMessagingException exception) { return new MockFirebaseMessagingClient(null, null, exception); } @@ -1018,11 +751,6 @@ public String send(Message message, boolean dryRun) throws FirebaseMessagingExce @Override public BatchResponse sendAll( List messages, boolean dryRun) throws FirebaseMessagingException { - lastBatch = messages; - isLastDryRun = dryRun; - if (exception != null) { - throw exception; - } return batchResponse; } } From d812307a3e23b12dad33f150ca6d9708b277f507 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:01:28 +0000 Subject: [PATCH 077/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.39.0 to 26.44.0 (#975) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 128632f95..3da4d9fe7 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.39.0 + 26.44.0 pom import From f6472d9f552771e8956d524061070b10940a3b5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:44:50 -0400 Subject: [PATCH 078/219] chore(deps): Bump netty.version from 4.1.110.Final to 4.1.112.Final (#980) Bumps `netty.version` from 4.1.110.Final to 4.1.112.Final. Updates `io.netty:netty-codec-http` from 4.1.110.Final to 4.1.112.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.110.Final...netty-4.1.112.Final) Updates `io.netty:netty-handler` from 4.1.110.Final to 4.1.112.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.110.Final...netty-4.1.112.Final) Updates `io.netty:netty-transport` from 4.1.110.Final to 4.1.112.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.110.Final...netty-4.1.112.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3da4d9fe7..8edc7d537 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.110.Final + 4.1.112.Final From 5465926ebecebfaee99f765b0425406e3dc5ad1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:47:58 +0000 Subject: [PATCH 079/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#986) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8edc7d537..f5d141ed0 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.7.0 + 3.10.0 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.7.0 + 3.10.0 attach-javadocs From 6817e15e43c204079e45c1081a0e04b015bf6554 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:50:05 +0000 Subject: [PATCH 080/219] chore(deps): Bump com.google.api-client:google-api-client-bom (#987) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f5d141ed0..cabf2260a 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.5.1 + 2.7.0 pom import From bbfa43a145d0b4a423b98414ae6fb83bdf226643 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:53:00 +0000 Subject: [PATCH 081/219] chore(deps): Bump org.apache.maven.plugins:maven-project-info-reports-plugin (#983) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cabf2260a..40185081f 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ maven-project-info-reports-plugin - 3.5.0 + 3.7.0 From 797a8af55a2992e0aa79420617c9b079571fba4b Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:55:28 -0400 Subject: [PATCH 082/219] fix: Limit default `ThreadPoolExecutor` thread count and remove deadlock scenario (#985) * fix: Remove possible deadlock with nested `callAsync()` * Set default `ThreadPoolExecutor` to a fixed thread pool * fix: add keepAliveTime to close idle threads * Added comments explainging the sendEachAsync change --- .../internal/FirebaseThreadManagers.java | 9 +++- .../firebase/messaging/FirebaseMessaging.java | 52 +++++++++++-------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java b/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java index 877450fcc..c14fbd611 100644 --- a/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java +++ b/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java @@ -23,7 +23,10 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,7 +87,11 @@ private static class DefaultThreadManager extends GlobalThreadManager { protected ExecutorService doInit() { ThreadFactory threadFactory = FirebaseScheduledExecutor.getThreadFactoryWithName( getThreadFactory(), "firebase-default-%d"); - return Executors.newCachedThreadPool(threadFactory); + + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 100, 60L, + TimeUnit.SECONDS, new LinkedBlockingQueue(), threadFactory); + threadPoolExecutor.allowCoreThreadTimeOut(true); + return threadPoolExecutor; } @Override diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index e16b6ac9d..870940f77 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -26,6 +26,7 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; @@ -159,7 +160,7 @@ protected String execute() throws FirebaseMessagingException { * no failures are only indicated by a {@link BatchResponse}. */ public BatchResponse sendEach(@NonNull List messages) throws FirebaseMessagingException { - return sendEachOp(messages, false).call(); + return sendEach(messages, false); } @@ -186,7 +187,11 @@ public BatchResponse sendEach(@NonNull List messages) throws FirebaseMe */ public BatchResponse sendEach( @NonNull List messages, boolean dryRun) throws FirebaseMessagingException { - return sendEachOp(messages, dryRun).call(); + try { + return sendEachOpAsync(messages, dryRun).get(); + } catch (InterruptedException | ExecutionException e) { + throw new FirebaseMessagingException(ErrorCode.CANCELLED, SERVICE_ID); + } } /** @@ -197,7 +202,7 @@ public BatchResponse sendEach( * the messages have been sent. */ public ApiFuture sendEachAsync(@NonNull List messages) { - return sendEachOp(messages, false).callAsync(app); + return sendEachOpAsync(messages, false); } /** @@ -209,32 +214,37 @@ public ApiFuture sendEachAsync(@NonNull List messages) { * the messages have been sent. */ public ApiFuture sendEachAsync(@NonNull List messages, boolean dryRun) { - return sendEachOp(messages, dryRun).callAsync(app); + return sendEachOpAsync(messages, dryRun); } - private CallableOperation sendEachOp( + // Returns an ApiFuture directly since this function is non-blocking. Individual child send + // requests are still called async and run in background threads. + private ApiFuture sendEachOpAsync( final List messages, final boolean dryRun) { final List immutableMessages = ImmutableList.copyOf(messages); checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); checkArgument(immutableMessages.size() <= 500, "messages list must not contain more than 500 elements"); - return new CallableOperation() { - @Override - protected BatchResponse execute() throws FirebaseMessagingException { - List> list = new ArrayList<>(); - for (Message message : immutableMessages) { - ApiFuture messageId = sendOpForSendResponse(message, dryRun).callAsync(app); - list.add(messageId); - } - try { - List responses = ApiFutures.allAsList(list).get(); - return new BatchResponseImpl(responses); - } catch (InterruptedException | ExecutionException e) { - throw new FirebaseMessagingException(ErrorCode.CANCELLED, SERVICE_ID); - } - } - }; + List> list = new ArrayList<>(); + for (Message message : immutableMessages) { + // Make async send calls per message + ApiFuture messageId = sendOpForSendResponse(message, dryRun).callAsync(app); + list.add(messageId); + } + + // Gather all futures and combine into a list + ApiFuture> responsesFuture = ApiFutures.allAsList(list); + + // Chain this future to wrap the eventual responses in a BatchResponse without blocking + // the main thread. This uses the current thread to execute, but since the transformation + // function is non-blocking the transformation itself is also non-blocking. + return ApiFutures.transform( + responsesFuture, + (responses) -> { + return new BatchResponseImpl(responses); + }, + MoreExecutors.directExecutor()); } private CallableOperation sendOpForSendResponse( From db445c58ad901fb5bccefa9ecfc9e22b77ef3c8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:21:33 -0400 Subject: [PATCH 083/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.44.0 to 26.45.0 (#989) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.44.0 to 26.45.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.44.0...v26.45.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 40185081f..57548b668 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.44.0 + 26.45.0 pom import From feed2c5bc731c7d171ea73ab2c5bd149671c1cc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:24:39 +0000 Subject: [PATCH 084/219] chore(deps): Bump org.slf4j:slf4j-api from 2.0.13 to 2.0.16 (#991) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 57548b668..cbebef211 100644 --- a/pom.xml +++ b/pom.xml @@ -438,7 +438,7 @@ org.slf4j slf4j-api - 2.0.13 + 2.0.16 io.netty From 183d4ecda7e8268f88224acedfc698210b0f4009 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:26:55 +0000 Subject: [PATCH 085/219] chore(deps): Bump org.apache.maven.plugins:maven-gpg-plugin (#992) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cbebef211..6ab199498 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ maven-gpg-plugin - 3.2.4 + 3.2.5 sign-artifacts From db91e3d1c3753635311919fb5edebe897379fb74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:11:19 -0400 Subject: [PATCH 086/219] chore(deps): Bump org.apache.maven.plugins:maven-failsafe-plugin (#990) Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.2.5 to 3.5.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.5.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6ab199498..f3ba72d2c 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.2.5 + 3.5.0 From 2229ec2cb591baf5b4a04e9e5e7713f6f7c5ed23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:13:32 +0000 Subject: [PATCH 087/219] chore(deps): Bump org.apache.maven.plugins:maven-surefire-plugin (#997) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f3ba72d2c..6cef75888 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.2.5 + 3.5.0 ${skipUTs} From b7c792bbfa7105343ba4130c59e67a4a0f14289c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:16:05 +0000 Subject: [PATCH 088/219] chore(deps-dev): Bump org.hamcrest:hamcrest from 2.2 to 3.0 (#996) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6cef75888..20428dd00 100644 --- a/pom.xml +++ b/pom.xml @@ -485,7 +485,7 @@ org.hamcrest hamcrest - 2.2 + 3.0 test From f4e50542148ced1321aeb4509b62a981b56284cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:18:47 +0000 Subject: [PATCH 089/219] chore(deps): Bump netty.version from 4.1.112.Final to 4.1.113.Final (#995) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 20428dd00..3c2963cde 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.112.Final + 4.1.113.Final From ce8e7fd0df9d8f89b8445ba767762589411e60c2 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 12 Sep 2024 15:07:12 -0400 Subject: [PATCH 090/219] chore: Update actions/upload-artifact (#998) --- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index b29cd2fc4..7bda5ba58 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -48,7 +48,7 @@ jobs: # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. - name: Archive artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: dist path: dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8a7050d8..ca1f042c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. - name: Archive artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: dist path: dist From ab46aaae81e790c6007afc456080773f8b74f897 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:59:48 -0400 Subject: [PATCH 091/219] feat: Add HTTP/2 enabled transport as default transport (#979) * Added HTTP/2 enabled transport and made it default. * fix: Use internal default transport * Added test coverage for timeouts * fix: lint * fix: Timeout tests no longer remove Firebase Apps for other integration tests. * Mirror tests from google java client and added more descriptive error messages. * fix: Remove `NO_CONNECT_URL` * debug IT Error * debug * debug * debug * debug * debug * Remove test debug * Address review comments * Use local server to test connect timeout and fix lint * Fix testConnectTimeoutGet test * Fix lint * remove deplicate tests * fix: catch `java.net.SocketTimeoutException` * Proxy tests --- pom.xml | 10 + .../ApacheHttp2AsyncEntityProducer.java | 150 +++++++ .../firebase/internal/ApacheHttp2Request.java | 147 +++++++ .../internal/ApacheHttp2Response.java | 100 +++++ .../internal/ApacheHttp2Transport.java | 125 ++++++ .../firebase/internal/ApiClientUtils.java | 2 +- .../internal/MockApacheHttp2AsyncClient.java | 73 ++++ .../google/firebase/FirebaseOptionsTest.java | 4 +- .../internal/ApacheHttp2TransportIT.java | 409 ++++++++++++++++++ .../internal/ApacheHttp2TransportTest.java | 379 ++++++++++++++++ .../internal/MockApacheHttp2Response.java | 188 ++++++++ .../FirebaseMessagingClientImplTest.java | 8 +- .../messaging/InstanceIdClientImplTest.java | 2 +- .../FirebaseRemoteConfigClientImplTest.java | 2 +- 14 files changed, 1590 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Request.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Response.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java create mode 100644 src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java create mode 100644 src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java create mode 100644 src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java create mode 100644 src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java diff --git a/pom.xml b/pom.xml index 3c2963cde..9fe0bfdd7 100644 --- a/pom.xml +++ b/pom.xml @@ -455,6 +455,11 @@ netty-transport ${netty.version} + + org.apache.httpcomponents.client5 + httpclient5 + 5.3.1 + @@ -488,5 +493,10 @@ 3.0 test + + org.mock-server + mockserver-junit-rule-no-dependencies + 5.14.0 + diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java new file mode 100644 index 000000000..9bdf208c7 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java @@ -0,0 +1,150 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.util.StreamingContent; +import com.google.common.annotations.VisibleForTesting; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { + private ByteBuffer bytebuf; + private ByteArrayOutputStream baos; + private final StreamingContent content; + private final ContentType contentType; + private final long contentLength; + private final String contentEncoding; + private final CompletableFuture writeFuture; + private final AtomicReference exception; + + public ApacheHttp2AsyncEntityProducer(StreamingContent content, ContentType contentType, + String contentEncoding, long contentLength, CompletableFuture writeFuture) { + this.content = content; + this.contentType = contentType; + this.contentEncoding = contentEncoding; + this.contentLength = contentLength; + this.writeFuture = writeFuture; + this.bytebuf = null; + + this.baos = new ByteArrayOutputStream((int) (contentLength < 0 ? 0 : contentLength)); + this.exception = new AtomicReference<>(); + } + + public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request, + CompletableFuture writeFuture) { + this( + request.getStreamingContent(), + ContentType.parse(request.getContentType()), + request.getContentEncoding(), + request.getContentLength(), + writeFuture); + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public String getContentType() { + return contentType != null ? contentType.toString() : null; + } + + @Override + public long getContentLength() { + return contentLength; + } + + @Override + public int available() { + return Integer.MAX_VALUE; + } + + @Override + public String getContentEncoding() { + return contentEncoding; + } + + @Override + public boolean isChunked() { + return contentLength == -1; + } + + @Override + public Set getTrailerNames() { + return null; + } + + @Override + public void produce(DataStreamChannel channel) throws IOException { + if (bytebuf == null) { + if (content != null) { + try { + content.writeTo(baos); + } catch (IOException e) { + failed(e); + throw e; + } + } + + this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); + } + + if (bytebuf.hasRemaining()) { + channel.write(bytebuf); + } + + if (!bytebuf.hasRemaining()) { + channel.endStream(); + writeFuture.complete(null); + releaseResources(); + } + } + + @Override + public void failed(Exception cause) { + if (exception.compareAndSet(null, cause)) { + releaseResources(); + writeFuture.completeExceptionally(cause); + } + } + + public final Exception getException() { + return exception.get(); + } + + @Override + public void releaseResources() { + if (bytebuf != null) { + bytebuf.clear(); + } + } + + @VisibleForTesting + ByteBuffer getBytebuf() { + return bytebuf; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java new file mode 100644 index 000000000..56c2c9034 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.common.annotations.VisibleForTesting; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.hc.client5.http.ConnectTimeoutException; +import org.apache.hc.client5.http.HttpHostConnectException; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http2.H2StreamResetException; +import org.apache.hc.core5.util.Timeout; + +final class ApacheHttp2Request extends LowLevelHttpRequest { + private final CloseableHttpAsyncClient httpAsyncClient; + private final SimpleRequestBuilder requestBuilder; + private SimpleHttpRequest request; + private final RequestConfig.Builder requestConfig; + private int writeTimeout; + private ApacheHttp2AsyncEntityProducer entityProducer; + + ApacheHttp2Request( + CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { + this.httpAsyncClient = httpAsyncClient; + this.requestBuilder = requestBuilder; + this.writeTimeout = 0; + + this.requestConfig = RequestConfig.custom() + .setRedirectsEnabled(false); + } + + @Override + public void addHeader(String name, String value) { + requestBuilder.addHeader(name, value); + } + + @Override + public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { + requestConfig + .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) + .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + } + + @Override + public void setWriteTimeout(int writeTimeout) throws IOException { + this.writeTimeout = writeTimeout; + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + // Set request configs + requestBuilder.setRequestConfig(requestConfig.build()); + + // Build request + request = requestBuilder.build(); + + // Make Producer + CompletableFuture writeFuture = new CompletableFuture<>(); + entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture); + + // Execute + final Future responseFuture = httpAsyncClient.execute( + new BasicRequestProducer(request, entityProducer), + SimpleResponseConsumer.create(), + new FutureCallback() { + @Override + public void completed(final SimpleHttpResponse response) { + } + + @Override + public void failed(final Exception exception) { + } + + @Override + public void cancelled() { + } + }); + + // Wait for write + try { + if (writeTimeout != 0) { + writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS); + } + } catch (TimeoutException e) { + throw new IOException("Write Timeout", e.getCause()); + } catch (Exception e) { + throw new IOException("Exception in write", e.getCause()); + } + + // Wait for response + try { + final SimpleHttpResponse response = responseFuture.get(); + return new ApacheHttp2Response(response); + } catch (ExecutionException e) { + if (e.getCause() instanceof ConnectTimeoutException + || e.getCause() instanceof SocketTimeoutException) { + throw new IOException("Connection Timeout", e.getCause()); + } else if (e.getCause() instanceof HttpHostConnectException) { + throw new IOException("Connection exception in request", e.getCause()); + } else if (e.getCause() instanceof H2StreamResetException) { + throw new IOException("Stream exception in request", e.getCause()); + } else { + throw new IOException("Unknown exception in request", e); + } + } catch (InterruptedException e) { + throw new IOException("Request Interrupted", e); + } catch (CancellationException e) { + throw new IOException("Request Cancelled", e); + } + } + + @VisibleForTesting + ApacheHttp2AsyncEntityProducer getEntityProducer() { + return entityProducer; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java new file mode 100644 index 000000000..4c05b0e03 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.common.annotations.VisibleForTesting; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; + +public class ApacheHttp2Response extends LowLevelHttpResponse { + private final SimpleHttpResponse response; + private final Header[] allHeaders; + + ApacheHttp2Response(SimpleHttpResponse response) { + this.response = response; + allHeaders = response.getHeaders(); + } + + @Override + public int getStatusCode() { + return response.getCode(); + } + + @Override + public InputStream getContent() throws IOException { + return new ByteArrayInputStream(response.getBodyBytes()); + } + + @Override + public String getContentEncoding() { + Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); + return contentEncodingHeader == null ? null : contentEncodingHeader.getValue(); + } + + @Override + public long getContentLength() { + String bodyText = response.getBodyText(); + return bodyText == null ? 0 : bodyText.length(); + } + + @Override + public String getContentType() { + ContentType contentType = response.getContentType(); + return contentType == null ? null : contentType.toString(); + } + + @Override + public String getReasonPhrase() { + return response.getReasonPhrase(); + } + + @Override + public String getStatusLine() { + return response.toString(); + } + + public String getHeaderValue(String name) { + return response.getLastHeader(name).getValue(); + } + + @Override + public String getHeaderValue(int index) { + return allHeaders[index].getValue(); + } + + @Override + public int getHeaderCount() { + return allHeaders.length; + } + + @Override + public String getHeaderName(int index) { + return allHeaders[index].getName(); + } + + @VisibleForTesting + public SimpleHttpResponse getResponse() { + return response; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java new file mode 100644 index 000000000..9c0e413fb --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java @@ -0,0 +1,125 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.http.HttpTransport; + +import java.io.IOException; +import java.net.ProxySelector; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.async.HttpAsyncClient; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.TlsConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.ssl.SSLContexts; + +/** + * HTTP/2 enabled async transport based on the Apache HTTP Client library + */ +public final class ApacheHttp2Transport extends HttpTransport { + + private final CloseableHttpAsyncClient httpAsyncClient; + private final boolean isMtls; + + public ApacheHttp2Transport() { + this(newDefaultHttpAsyncClient(), false); + } + + public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient) { + this(httpAsyncClient, false); + } + + public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient, boolean isMtls) { + this.httpAsyncClient = httpAsyncClient; + this.isMtls = isMtls; + + httpAsyncClient.start(); + } + + public static CloseableHttpAsyncClient newDefaultHttpAsyncClient() { + return defaultHttpAsyncClientBuilder().build(); + } + + public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() { + PoolingAsyncClientConnectionManager connectionManager = + PoolingAsyncClientConnectionManagerBuilder.create() + // Set Max total connections to match google api client limits + // https://github.com/googleapis/google-http-java-client/blob/f9d4e15bd3c784b1fd3b0f3468000a91c6f79715/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java#L151 + .setMaxConnTotal(200) + // Set max connections per route to match the concurrent stream limit of the FCM backend. + .setMaxConnPerRoute(100) + .setDefaultConnectionConfig( + ConnectionConfig.custom().setTimeToLive(-1, TimeUnit.MILLISECONDS).build()) + .setDefaultTlsConfig( + TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.NEGOTIATE).build()) + .setTlsStrategy(ClientTlsStrategyBuilder.create() + .setSslContext(SSLContexts.createSystemDefault()) + .build()) + .build(); + + return HttpAsyncClientBuilder.create() + // Set maxConcurrentStreams to 100 to match the concurrent stream limit of the FCM backend. + .setH2Config(H2Config.custom().setMaxConcurrentStreams(100).build()) + .setHttp1Config(Http1Config.DEFAULT) + .setConnectionManager(connectionManager) + .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())) + .disableRedirectHandling() + .disableAutomaticRetries(); + } + + @Override + public boolean supportsMethod(String method) { + return true; + } + + @Override + protected ApacheHttp2Request buildRequest(String method, String url) { + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(method).setUri(url); + return new ApacheHttp2Request(httpAsyncClient, requestBuilder); + } + + /** + * Gracefully shuts down the connection manager and releases allocated resources. This closes all + * connections, whether they are currently used or not. + */ + @Override + public void shutdown() throws IOException { + httpAsyncClient.close(CloseMode.GRACEFUL); + } + + /** Returns the Apache HTTP client. */ + public HttpAsyncClient getHttpClient() { + return httpAsyncClient; + } + + /** Returns if the underlying HTTP client is mTLS. */ + @Override + public boolean isMtls() { + return isMtls; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index e586bd11f..339726105 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -88,6 +88,6 @@ public static JsonFactory getDefaultJsonFactory() { } public static HttpTransport getDefaultTransport() { - return Utils.getDefaultTransport(); + return new ApacheHttp2Transport(); } } diff --git a/src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java b/src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java new file mode 100644 index 000000000..c859ec485 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import java.io.IOException; +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.HandlerFactory; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.reactor.IOReactorStatus; +import org.apache.hc.core5.util.TimeValue; + +public class MockApacheHttp2AsyncClient extends CloseableHttpAsyncClient { + + @Override + public void close(CloseMode closeMode) { + } + + @Override + public void close() throws IOException { + } + + @Override + public void start() { + } + + @Override + public IOReactorStatus getStatus() { + return null; + } + + @Override + public void awaitShutdown(TimeValue waitTime) throws InterruptedException { + } + + @Override + public void initiateShutdown() { + } + + @Override + protected Future doExecute(HttpHost target, AsyncRequestProducer requestProducer, + AsyncResponseConsumer responseConsumer, + HandlerFactory pushHandlerFactory, + HttpContext context, FutureCallback callback) { + return null; + } + + @Override + public void register(String hostname, String uriPattern, Supplier supplier) { + } +} diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index 0a4e9e81a..05613e98a 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -24,12 +24,12 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.firestore.FirestoreOptions; +import com.google.firebase.internal.ApacheHttp2Transport; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.io.IOException; @@ -74,7 +74,7 @@ protected ThreadFactory getThreadFactory() { @Test public void createOptionsWithAllValuesSet() throws IOException { GsonFactory jsonFactory = new GsonFactory(); - NetHttpTransport httpTransport = new NetHttpTransport(); + ApacheHttp2Transport httpTransport = new ApacheHttp2Transport(); FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder().build(); FirebaseOptions firebaseOptions = FirebaseOptions.builder() diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java new file mode 100644 index 000000000..c8fbe5533 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -0,0 +1,409 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockserver.model.Header.header; +import static org.mockserver.model.HttpForward.forward; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.GenericData; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.auth.MockGoogleCredentials; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpForward.Scheme; +import org.mockserver.socket.PortFactory; + +public class ApacheHttp2TransportIT { + private static FirebaseApp app; + private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); + private static final ImmutableMap payload = + ImmutableMap.of("foo", "bar"); + + // Sets a 5 second delay before server response to simulate a slow network that + // results in a read timeout. + private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/5"; + private static final String GET_URL = "https://nghttp2.org/httpbin/get"; + private static final String POST_URL = "https://nghttp2.org/httpbin/post"; + + private static ServerSocket serverSocket; + private static Socket fillerSocket; + private static int port; + + private static ClientAndServer mockProxy; + private static ClientAndServer mockServer; + + @BeforeClass + public static void setUpClass() throws IOException { + // Start server socket with a backlog queue of 1 and a automatically assigned + // port + serverSocket = new ServerSocket(0, 1); + port = serverSocket.getLocalPort(); + // Fill the backlog queue to force socket to ignore future connections + fillerSocket = new Socket(); + fillerSocket.connect(serverSocket.getLocalSocketAddress()); + } + + @AfterClass + public static void cleanUpClass() throws IOException { + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + if (fillerSocket != null && !fillerSocket.isClosed()) { + fillerSocket.close(); + } + } + + @After + public void cleanup() { + if (app != null) { + app.delete(); + } + + if (mockProxy != null && mockProxy.isRunning()) { + mockProxy.close(); + } + + if (mockServer != null && mockServer.isRunning()) { + mockServer.close(); + } + + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); + } + + @Test(timeout = 10_000L) + public void testUnauthorizedGetRequest() throws FirebaseException { + ErrorHandlingHttpClient httpClient = getHttpClient(false); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); + IncomingHttpResponse response = httpClient.send(request); + assertEquals(200, response.getStatusCode()); + } + + @Test(timeout = 10_000L) + public void testUnauthorizedPostRequest() throws FirebaseException { + ErrorHandlingHttpClient httpClient = getHttpClient(false); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); + GenericData body = httpClient.sendAndParse(request, GenericData.class); + assertEquals("{\"foo\":\"bar\"}", body.get("data")); + } + + @Test(timeout = 10_000L) + public void testConnectTimeoutAuthorizedGet() throws FirebaseException { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setConnectTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest("https://localhost:" + port); + + try { + httpClient.send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Connection Timeout", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test(timeout = 10_000L) + public void testConnectTimeoutAuthorizedPost() throws FirebaseException { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setConnectTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest("https://localhost:" + port, payload); + + try { + httpClient.send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Connection Timeout", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test(timeout = 10_000L) + public void testReadTimeoutAuthorizedGet() throws FirebaseException { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setReadTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); + + try { + httpClient.send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Stream exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test(timeout = 10_000L) + public void testReadTimeoutAuthorizedPost() throws FirebaseException { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setReadTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); + + try { + httpClient.send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Stream exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test(timeout = 10_000L) + public void testWriteTimeoutAuthorizedGet() throws FirebaseException { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setWriteTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); + + try { + httpClient.send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Write Timeout", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test(timeout = 10_000L) + public void testWriteTimeoutAuthorizedPost() throws FirebaseException { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setWriteTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); + + try { + httpClient.send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Write Timeout", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test(timeout = 10_000L) + public void testRequestShouldNotFollowRedirects() throws IOException { + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + ApacheHttp2Request request = transport.buildRequest("GET", + "https://google.com"); + LowLevelHttpResponse response = request.execute(); + + assertEquals(301, response.getStatusCode()); + assert (response instanceof ApacheHttp2Response); + assertEquals("https://www.google.com/", ((ApacheHttp2Response) response).getHeaderValue("location")); + } + + @Test(timeout = 10_000L) + public void testRequestCanSetHeaders() { + final AtomicBoolean interceptorCalled = new AtomicBoolean(false); + CloseableHttpAsyncClient client = HttpAsyncClients.custom() + .addRequestInterceptorFirst( + new HttpRequestInterceptor() { + @Override + public void process( + HttpRequest request, EntityDetails details, HttpContext context) + throws HttpException, IOException { + Header header = request.getFirstHeader("foo"); + assertNotNull("Should have found header", header); + assertEquals("bar", header.getValue()); + interceptorCalled.set(true); + throw new IOException("cancelling request"); + } + }) + .build(); + + ApacheHttp2Transport transport = new ApacheHttp2Transport(client); + ApacheHttp2Request request = transport.buildRequest("GET", "http://www.google.com"); + request.addHeader("foo", "bar"); + try { + request.execute(); + fail("should not actually make the request"); + } catch (IOException exception) { + assertEquals("Unknown exception in request", exception.getMessage()); + } + assertTrue("Expected to have called our test interceptor", interceptorCalled.get()); + } + + @Test(timeout = 10_000L) + public void testVerifyProxyIsRespected() { + try { + System.setProperty("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", "8080"); + + HttpTransport transport = new ApacheHttp2Transport(); + transport.createRequestFactory().buildGetRequest(new GenericUrl(GET_URL)).execute(); + fail("No exception thrown for HTTP error response"); + } catch (IOException e) { + assertEquals("Connection exception in request", e.getMessage()); + assertTrue(e.getCause().getMessage().contains("localhost:8080")); + } + } + + @Test(timeout = 10_000L) + public void testProxyMockHttp() throws Exception { + // Start MockServer + mockProxy = ClientAndServer.startClientAndServer(PortFactory.findFreePort()); + mockServer = ClientAndServer.startClientAndServer(PortFactory.findFreePort()); + + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", mockProxy.getPort().toString()); + + // Configure proxy to receieve requests and forward them to a mock destination + // server + mockProxy + .when( + request()) + .forward( + forward() + .withHost("localhost") + .withPort(mockServer.getPort()) + .withScheme(Scheme.HTTP)); + + // Configure server to listen and respond + mockServer + .when( + request()) + .respond( + response() + .withStatusCode(200) + .withBody("Expected server response")); + + // Send a request through the proxy + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setWriteTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest("http://www.google.com"); + IncomingHttpResponse response = httpClient.send(request); + + // Verify that the proxy received request with destination host + mockProxy.verify( + request() + .withMethod("GET") + .withPath("/") + .withHeader(header("Host", "www.google.com"))); + + // Verify the forwarded request is received by the server + mockServer.verify( + request() + .withMethod("GET") + .withPath("/")); + + // Verify response + assertEquals(200, response.getStatusCode()); + assertEquals(response.getContent(), "Expected server response"); + } + + private static ErrorHandlingHttpClient getHttpClient(boolean authorized, + FirebaseApp app) { + HttpRequestFactory requestFactory; + if (authorized) { + requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + } else { + requestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); + } + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + TestHttpErrorHandler errorHandler = new TestHttpErrorHandler(); + return new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler); + } + + private static ErrorHandlingHttpClient getHttpClient(boolean authorized) { + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .build(), "test-app"); + return getHttpClient(authorized, app); + } + + private static class TestHttpErrorHandler implements HttpErrorHandler { + @Override + public FirebaseException handleIOException(IOException e) { + return new FirebaseException( + ErrorCode.UNKNOWN, "IO error: " + e.getMessage(), e); + } + + @Override + public FirebaseException handleHttpResponseException( + HttpResponseException e, IncomingHttpResponse response) { + return new FirebaseException( + ErrorCode.INTERNAL, "Example error message: " + e.getContent(), e, response); + } + + @Override + public FirebaseException handleParseException(IOException e, IncomingHttpResponse response) { + return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", e, response); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java new file mode 100644 index 000000000..fcaf563f3 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java @@ -0,0 +1,379 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.util.ByteArrayStreamingContent; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.async.HttpAsyncClient; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestMapper; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.impl.bootstrap.HttpServer; +import org.apache.hc.core5.http.impl.io.HttpService; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler; +import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.HandlerFactory; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.junit.Assert; +import org.junit.Test; + +public class ApacheHttp2TransportTest { + @Test + public void testContentLengthSet() throws Exception { + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) + .setUri("http://www.google.com"); + + ApacheHttp2Request request = new ApacheHttp2Request( + new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + } + }, requestBuilder); + + HttpContent content = new ByteArrayContent("text/plain", + "sample".getBytes(StandardCharsets.UTF_8)); + request.setStreamingContent(content); + request.setContentLength(content.getLength()); + request.execute(); + + assertFalse(request.getEntityProducer().isChunked()); + assertEquals(6, request.getEntityProducer().getContentLength()); + } + + @Test + public void testChunked() throws Exception { + byte[] buf = new byte[300]; + Arrays.fill(buf, (byte) ' '); + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) + .setUri("http://www.google.com"); + ApacheHttp2Request request = new ApacheHttp2Request( + new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + } + }, requestBuilder); + + HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf)); + request.setStreamingContent(content); + request.execute(); + + assertTrue(request.getEntityProducer().isChunked()); + assertEquals(-1, request.getEntityProducer().getContentLength()); + } + + @Test + public void testExecute() throws Exception { + SimpleHttpResponse simpleHttpResponse = SimpleHttpResponse.create(200, new byte[] { 1, 2, 3 }); + + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) + .setUri("http://www.google.com"); + + ApacheHttp2Request request = new ApacheHttp2Request( + new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(simpleHttpResponse); + } + }, requestBuilder); + LowLevelHttpResponse response = request.execute(); + assertTrue(response instanceof ApacheHttp2Response); + + // we confirm that the simple response we prepared in this test is the same as + // the content's response + assertTrue(response.getContent() instanceof ByteArrayInputStream); + assertEquals(simpleHttpResponse, ((ApacheHttp2Response) response).getResponse()); + // No need to cloase ByteArrayInputStream since close() has no effect. + } + + @Test + public void testApacheHttpTransport() { + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + checkHttpTransport(transport); + assertFalse(transport.isMtls()); + } + + @Test + public void testApacheHttpTransportWithParam() { + HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom(); + ApacheHttp2Transport transport = new ApacheHttp2Transport(clientBuilder.build(), true); + checkHttpTransport(transport); + assertTrue(transport.isMtls()); + } + + @Test + public void testNewDefaultHttpClient() { + HttpAsyncClient client = ApacheHttp2Transport.newDefaultHttpAsyncClient(); + checkHttpClient(client); + } + + @Test + public void testDefaultHttpClientBuilder() { + HttpAsyncClientBuilder clientBuilder = ApacheHttp2Transport.defaultHttpAsyncClientBuilder(); + HttpAsyncClient client = clientBuilder.build(); + checkHttpClient(client); + } + + private void checkHttpTransport(ApacheHttp2Transport transport) { + assertNotNull(transport); + HttpAsyncClient client = transport.getHttpClient(); + checkHttpClient(client); + } + + private void checkHttpClient(HttpAsyncClient client) { + assertNotNull(client); + } + + @Test + public void testRequestsWithContent() throws IOException { + // This test confirms that we can set the content on any type of request + CloseableHttpAsyncClient mockClient = new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + } + }; + ApacheHttp2Transport transport = new ApacheHttp2Transport(mockClient); + + // Test GET. + execute(transport.buildRequest("GET", "http://www.test.url")); + // Test DELETE. + execute(transport.buildRequest("DELETE", "http://www.test.url")); + // Test HEAD. + execute(transport.buildRequest("HEAD", "http://www.test.url")); + // Test PATCH. + execute(transport.buildRequest("PATCH", "http://www.test.url")); + // Test PUT. + execute(transport.buildRequest("PUT", "http://www.test.url")); + // Test POST. + execute(transport.buildRequest("POST", "http://www.test.url")); + // Test PATCH. + execute(transport.buildRequest("PATCH", "http://www.test.url")); + } + + @Test + public void testNormalizedUrl() throws IOException { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + // Extract the request URI and convert to bytes + byte[] responseData = request.getRequestUri().getBytes(StandardCharsets.UTF_8); + + // Set the response headers (status code and content length) + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + + // Set the response entity (body) + ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpResponse response = transport.createRequestFactory() + .buildGetRequest(testUrl) + .execute(); + assertEquals(200, response.getStatusCode()); + assertEquals("/foo//bar", response.parseAsString()); + } + } + + @Test + public void testReadErrorStream() throws IOException { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpRequest getRequest = transport.createRequestFactory() + .buildGetRequest(testUrl); + getRequest.setThrowExceptionOnExecuteError(false); + com.google.api.client.http.HttpResponse response = getRequest.execute(); + assertEquals(403, response.getStatusCode()); + assertEquals("Forbidden", response.parseAsString()); + } + } + + @Test + public void testReadErrorStream_withException() throws IOException { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpRequest getRequest = transport.createRequestFactory() + .buildGetRequest(testUrl); + try { + getRequest.execute(); + Assert.fail(); + } catch (HttpResponseException ex) { + assertEquals("Forbidden", ex.getContent()); + } + } + } + + private void execute(ApacheHttp2Request request) throws IOException { + byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8); + request.setStreamingContent(new ByteArrayStreamingContent(bytes)); + request.setContentType("text/html"); + request.setContentLength(bytes.length); + request.execute(); + } + + private static class FakeServer implements AutoCloseable { + private final HttpServer server; + + FakeServer(final HttpRequestHandler httpHandler) throws IOException { + HttpRequestMapper mapper = new HttpRequestMapper() { + @Override + public HttpRequestHandler resolve(HttpRequest request, HttpContext context) + throws HttpException { + return httpHandler; + } + }; + server = new HttpServer( + 0, + HttpService.builder() + .withHttpProcessor( + new HttpProcessor() { + @Override + public void process( + HttpRequest request, EntityDetails entity, HttpContext context) + throws HttpException, IOException { + } + + @Override + public void process( + HttpResponse response, EntityDetails entity, HttpContext context) + throws HttpException, IOException { + } + }) + .withHttpServerRequestHandler(new BasicHttpServerRequestHandler(mapper)) + .build(), + null, + null, + null, + null, + null, + null); + server.start(); + } + + public int getPort() { + return server.getLocalPort(); + } + + @Override + public void close() { + server.initiateShutdown(); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java b/src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java new file mode 100644 index 000000000..0f9c04042 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java @@ -0,0 +1,188 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; + +public class MockApacheHttp2Response implements HttpResponse { + List

headers = new ArrayList<>(); + int code = 200; + + @Override + public void setVersion(ProtocolVersion version) { + } + + @Override + public ProtocolVersion getVersion() { + return null; + } + + @Override + public void addHeader(Header header) { + headers.add(header); + } + + @Override + public void addHeader(String name, Object value) { + addHeader(newHeader(name, value)); + } + + private Header newHeader(String key, Object value) { + return new Header() { + @Override + public boolean isSensitive() { + return false; + } + + @Override + public String getName() { + return key; + } + + @Override + public String getValue() { + return value.toString(); + } + }; + } + + @Override + public void setHeader(Header header) { + if (headers.contains(header)) { + int index = headers.indexOf(header); + headers.set(index, header); + } else { + addHeader(header); + } + } + + @Override + public void setHeader(String name, Object value) { + setHeader(newHeader(name, value)); + } + + @Override + public void setHeaders(Header... headers) { + for (Header header : headers) { + setHeader(header); + } + } + + @Override + public boolean removeHeader(Header header) { + if (headers.contains(header)) { + headers.remove(headers.indexOf(header)); + return true; + } + return false; + } + + @Override + public boolean removeHeaders(String name) { + int initialSize = headers.size(); + for (Header header : headers.stream().filter(h -> h.getName() == name) + .collect(Collectors.toList())) { + removeHeader(header); + } + return headers.size() < initialSize; + } + + @Override + public boolean containsHeader(String name) { + return headers.stream().anyMatch(h -> h.getName() == name); + } + + @Override + public int countHeaders(String name) { + return headers.size(); + } + + @Override + public Header getFirstHeader(String name) { + return headers.stream().findFirst().orElse(null); + } + + @Override + public Header getHeader(String name) throws ProtocolException { + return headers.stream().filter(h -> h.getName() == name).findFirst().orElse(null); + } + + @Override + public Header[] getHeaders() { + return headers.toArray(new Header[0]); + } + + @Override + public Header[] getHeaders(String name) { + return headers.stream() + .filter(h -> h.getName() == name) + .collect(Collectors.toList()) + .toArray(new Header[0]); + } + + @Override + public Header getLastHeader(String name) { + return headers.isEmpty() ? null : headers.get(headers.size() - 1); + } + + @Override + public Iterator
headerIterator() { + return headers.iterator(); + } + + @Override + public Iterator
headerIterator(String name) { + return headers.stream().filter(h -> h.getName() == name).iterator(); + } + + @Override + public int getCode() { + return this.code; + } + + @Override + public void setCode(int code) { + this.code = code; + } + + @Override + public String getReasonPhrase() { + return null; + } + + @Override + public void setReasonPhrase(String reason) { + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public void setLocale(Locale loc) { + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 3180aea01..b75d9a3d6 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -368,7 +368,7 @@ private FirebaseMessagingClientImpl initMessagingClient( .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) .setRequestFactory(transport.createRequestFactory()) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) .setResponseInterceptor(interceptor) .build(); } @@ -379,7 +379,7 @@ private FirebaseMessagingClientImpl initClientWithFaultyTransport() { .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) .setRequestFactory(transport.createRequestFactory()) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) .build(); } @@ -405,8 +405,8 @@ private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { return FirebaseMessagingClientImpl.builder() .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); } private void checkExceptionFromHttpResponse( diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index e7222ccb5..1a140680f 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -358,7 +358,7 @@ public void testRequestFactoryIsNull() { @Test(expected = NullPointerException.class) public void testJsonFactoryIsNull() { - new InstanceIdClientImpl(Utils.getDefaultTransport().createRequestFactory(), null); + new InstanceIdClientImpl(ApiClientUtils.getDefaultTransport().createRequestFactory(), null); } @Test diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index f2d5c8126..0a04809cf 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -1173,7 +1173,7 @@ private FirebaseRemoteConfigClientImpl.Builder fullyPopulatedBuilder() { return FirebaseRemoteConfigClientImpl.builder() .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); } private void checkGetRequestHeader(HttpRequest request) { From 686287d7607d484003339a7cc86fbbce1e2e18f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:38:48 -0400 Subject: [PATCH 092/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.45.0 to 26.48.0 (#1008) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.45.0 to 26.48.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.45.0...v26.48.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9fe0bfdd7..c0a217b70 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.45.0 + 26.48.0 pom import From 0cb7af3552d64a084fb00dfbe06b7ac63bfd9681 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:40:51 +0000 Subject: [PATCH 093/219] chore(deps): Bump org.apache.maven.plugins:maven-gpg-plugin (#1005) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c0a217b70..cb44a39da 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ maven-gpg-plugin - 3.2.5 + 3.2.7 sign-artifacts From f29579fc15952351250d6ef0c7d8236abdce8685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:43:52 +0000 Subject: [PATCH 094/219] chore(deps): Bump org.codehaus.mojo:exec-maven-plugin (#999) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cb44a39da..2b63fb392 100644 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.codehaus.mojo exec-maven-plugin - 3.3.0 + 3.4.1 test From c7005d7a3c463132dcd63920c09a7fb53c5feb54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:17:47 -0400 Subject: [PATCH 095/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#1011) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.10.0 to 3.10.1. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.10.0...maven-javadoc-plugin-3.10.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2b63fb392..ad73afd11 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.10.0 + 3.10.1 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.10.0 + 3.10.1 attach-javadocs From f8699ae4b162521f2816e0a4023de813c369a070 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:20:39 +0000 Subject: [PATCH 096/219] chore(deps): Bump netty.version from 4.1.113.Final to 4.1.114.Final (#1010) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ad73afd11..f741396d9 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.113.Final + 4.1.114.Final From c0d87b953e58834c10d63da1eb7dc3d2168aae62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:22:54 +0000 Subject: [PATCH 097/219] chore(deps): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1009) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f741396d9..7a9fcbc4c 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.5.0 + 3.5.1 From edfa49c328120d4034dbc00053f92f397c6f05e6 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:47:48 -0400 Subject: [PATCH 098/219] [chore] Release 9.4.0 (#1013) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7a9fcbc4c..62793211e 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.3.0 + 9.4.0 jar firebase-admin From 2a58a9f114641a478053dd1e4937ffed6e75e9ae Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:34:55 -0400 Subject: [PATCH 099/219] [chore] Release 9.4.0 Take 2 (#1014) --- .github/resources/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml index 847ea52d0..f3bc754e2 100644 --- a/.github/resources/settings.xml +++ b/.github/resources/settings.xml @@ -21,7 +21,7 @@ gpg - 77A3CB7D41F06A4512BA8EA5AA4E6ADF6A05C58E + 0A05D8FAD4287A36C53BE07714D6B82AEB1DD39C ${env.GPG_PASSPHRASE} From 2469a09f8b8aeff517fceb00734afca281132da5 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:45:54 -0400 Subject: [PATCH 100/219] fix: Remove mockserver dependency (#1022) --- pom.xml | 5 -- .../internal/ApacheHttp2TransportIT.java | 75 ------------------- 2 files changed, 80 deletions(-) diff --git a/pom.xml b/pom.xml index 62793211e..3732011ea 100644 --- a/pom.xml +++ b/pom.xml @@ -493,10 +493,5 @@ 3.0 test - - org.mock-server - mockserver-junit-rule-no-dependencies - 5.14.0 - diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index c8fbe5533..047e1de0b 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -21,10 +21,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockserver.model.Header.header; -import static org.mockserver.model.HttpForward.forward; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequestFactory; @@ -59,10 +55,6 @@ import org.junit.BeforeClass; import org.junit.Test; -import org.mockserver.integration.ClientAndServer; -import org.mockserver.model.HttpForward.Scheme; -import org.mockserver.socket.PortFactory; - public class ApacheHttp2TransportIT { private static FirebaseApp app; private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); @@ -79,9 +71,6 @@ public class ApacheHttp2TransportIT { private static Socket fillerSocket; private static int port; - private static ClientAndServer mockProxy; - private static ClientAndServer mockServer; - @BeforeClass public static void setUpClass() throws IOException { // Start server socket with a backlog queue of 1 and a automatically assigned @@ -109,14 +98,6 @@ public void cleanup() { app.delete(); } - if (mockProxy != null && mockProxy.isRunning()) { - mockProxy.close(); - } - - if (mockServer != null && mockServer.isRunning()) { - mockServer.close(); - } - System.clearProperty("http.proxyHost"); System.clearProperty("http.proxyPort"); System.clearProperty("https.proxyHost"); @@ -311,62 +292,6 @@ public void testVerifyProxyIsRespected() { } } - @Test(timeout = 10_000L) - public void testProxyMockHttp() throws Exception { - // Start MockServer - mockProxy = ClientAndServer.startClientAndServer(PortFactory.findFreePort()); - mockServer = ClientAndServer.startClientAndServer(PortFactory.findFreePort()); - - System.setProperty("http.proxyHost", "localhost"); - System.setProperty("http.proxyPort", mockProxy.getPort().toString()); - - // Configure proxy to receieve requests and forward them to a mock destination - // server - mockProxy - .when( - request()) - .forward( - forward() - .withHost("localhost") - .withPort(mockServer.getPort()) - .withScheme(Scheme.HTTP)); - - // Configure server to listen and respond - mockServer - .when( - request()) - .respond( - response() - .withStatusCode(200) - .withBody("Expected server response")); - - // Send a request through the proxy - app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .setWriteTimeout(100) - .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest("http://www.google.com"); - IncomingHttpResponse response = httpClient.send(request); - - // Verify that the proxy received request with destination host - mockProxy.verify( - request() - .withMethod("GET") - .withPath("/") - .withHeader(header("Host", "www.google.com"))); - - // Verify the forwarded request is received by the server - mockServer.verify( - request() - .withMethod("GET") - .withPath("/")); - - // Verify response - assertEquals(200, response.getStatusCode()); - assertEquals(response.getContent(), "Expected server response"); - } - private static ErrorHandlingHttpClient getHttpClient(boolean authorized, FirebaseApp app) { HttpRequestFactory requestFactory; From 0523993e49d5d8241cb9251f3b488636a2a54f8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:26:25 -0400 Subject: [PATCH 101/219] chore(deps): Bump org.apache.maven.plugins:maven-surefire-plugin (#1021) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.0 to 3.5.1. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.0...surefire-3.5.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3732011ea..a146c39da 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.5.0 + 3.5.1 ${skipUTs} From f581e5325c7595dfa40be348bbf2b169382f7491 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:58:06 -0400 Subject: [PATCH 102/219] [chore] Release 9.4.1 (#1024) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a146c39da..6136770a4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.4.0 + 9.4.1 jar firebase-admin From 79b880950ac97763fc361fe2a3e7fa085bca1539 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:27:03 -0400 Subject: [PATCH 103/219] chore(deps): Bump org.apache.maven.plugins:maven-project-info-reports-plugin (#1026) Bumps [org.apache.maven.plugins:maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 3.7.0 to 3.8.0. - [Commits](https://github.com/apache/maven-project-info-reports-plugin/compare/maven-project-info-reports-plugin-3.7.0...maven-project-info-reports-plugin-3.8.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-project-info-reports-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6136770a4..ce2ff9339 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ maven-project-info-reports-plugin - 3.7.0 + 3.8.0 From feaa092802ab1a7b0170eb601c12eedb39c81eeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:30:12 +0000 Subject: [PATCH 104/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.48.0 to 26.49.0 (#1025) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ce2ff9339..39548b0fa 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.48.0 + 26.49.0 pom import From 88b8e5d5b78d1494541a7ec22759d38a788256a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:46:25 -0500 Subject: [PATCH 105/219] chore(deps): Bump org.apache.maven.plugins:maven-surefire-plugin (#1038) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.1 to 3.5.2. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 39548b0fa..102764938 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.5.1 + 3.5.2 ${skipUTs} From 0dff988c2bd2642ba83472c8110e8573d459a532 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:49:34 +0000 Subject: [PATCH 106/219] chore(deps): Bump org.codehaus.mojo:exec-maven-plugin (#1037) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 102764938..e0340a794 100644 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.codehaus.mojo exec-maven-plugin - 3.4.1 + 3.5.0 test From b4b2fc12510909b6515cc34ded8b8b5ba34ee056 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:15:13 -0500 Subject: [PATCH 107/219] chore: Add `X-Goog-Api-Client` metric header to outgoing requests (#1039) --- .../firebase/internal/ErrorHandlingHttpClient.java | 1 + src/main/java/com/google/firebase/internal/SdkUtils.java | 8 ++++++++ .../google/firebase/auth/FirebaseUserManagerTest.java | 2 ++ .../google/firebase/auth/internal/CryptoSignersTest.java | 5 +++++ .../auth/multitenancy/FirebaseTenantClientTest.java | 1 + .../auth/multitenancy/TenantAwareFirebaseAuthTest.java | 9 +++++++++ .../com/google/firebase/iid/FirebaseInstanceIdTest.java | 2 ++ .../firebase/internal/ErrorHandlingHttpClientTest.java | 1 + .../messaging/FirebaseMessagingClientImplTest.java | 1 + .../firebase/messaging/InstanceIdClientImplTest.java | 3 +++ .../FirebaseProjectManagementServiceImplTest.java | 1 + .../remoteconfig/FirebaseRemoteConfigClientImplTest.java | 3 +++ 12 files changed, 37 insertions(+) diff --git a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java index 5efdd0ec2..cccd0cedb 100644 --- a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java +++ b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java @@ -89,6 +89,7 @@ public void sendAndParse(HttpRequestInfo requestInfo, Object destination) throws } public IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T { + requestInfo.addHeader("X-Goog-Api-Client", SdkUtils.getMetricsHeader()); HttpRequest request = createHttpRequest(requestInfo); HttpResponse response = null; diff --git a/src/main/java/com/google/firebase/internal/SdkUtils.java b/src/main/java/com/google/firebase/internal/SdkUtils.java index 2fa5e5767..cd9619087 100644 --- a/src/main/java/com/google/firebase/internal/SdkUtils.java +++ b/src/main/java/com/google/firebase/internal/SdkUtils.java @@ -39,6 +39,14 @@ public static String getVersion() { return SDK_VERSION; } + public static String getJavaVersion() { + return System.getProperty("java.version"); + } + + public static String getMetricsHeader() { + return String.format("gl-java/%s fire-admin/%s", getJavaVersion(), getVersion()); + } + private static String loadSdkVersion() { try (InputStream in = SdkUtils.class.getClassLoader() .getResourceAsStream(ADMIN_SDK_PROPERTIES)) { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 41085d181..c8c733ce4 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -3026,6 +3026,8 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + assertEquals(SdkUtils.getMetricsHeader(), headers.get("X-Goog-Api-Client")); + } private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java index 679fe5a3c..17af1eb76 100644 --- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -39,6 +39,7 @@ import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestResponseInterceptor; @@ -87,6 +88,8 @@ public void testIAMCryptoSigner() throws Exception { final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "test-service-account@iam.gserviceaccount.com:signBlob"; assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); } @Test @@ -173,6 +176,7 @@ public void testMetadataService() throws Exception { HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(url, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); } @Test @@ -203,6 +207,7 @@ public void testExplicitServiceAccountEmail() throws Exception { HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(url, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); } @Test diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java index eca55ee66..cdb5852d0 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -353,6 +353,7 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + assertEquals(SdkUtils.getMetricsHeader(), headers.get("X-Goog-Api-Client")); } private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java index 68e35492e..4cf36a3d7 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -38,6 +38,7 @@ import com.google.firebase.auth.MockTokenVerifier; import com.google.firebase.auth.SessionCookieOptions; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; import java.io.ByteArrayOutputStream; @@ -85,6 +86,7 @@ public void testCreateSessionCookieAsync() throws Exception { assertEquals("testToken", parsed.get("idToken")); assertEquals(new BigDecimal(3600), parsed.get("validDuration")); checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + checkHeaders(interceptor); } @Test @@ -121,6 +123,7 @@ public void testCreateSessionCookie() throws Exception { assertEquals("testToken", parsed.get("idToken")); assertEquals(new BigDecimal(3600), parsed.get("validDuration")); checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + checkHeaders(interceptor); } @Test @@ -212,6 +215,7 @@ public void testVerifySessionCookieWithCheckRevoked() throws FirebaseAuthExcepti assertEquals("uid", token.getUid()); assertEquals("cookie", tokenVerifier.getLastTokenString()); checkUrl(interceptor, AUTH_BASE_URL + "/accounts:lookup"); + checkHeaders(interceptor); } @Test @@ -290,4 +294,9 @@ private static void checkUrl(TestResponseInterceptor interceptor, String url) { assertEquals(HttpMethods.POST, request.getRequestMethod()); assertEquals(url, request.getUrl().getRawPath()); } + + private static void checkHeaders(TestResponseInterceptor interceptor) { + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); + } } diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index 27864d992..f563d87dd 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -38,6 +38,7 @@ import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; @@ -174,6 +175,7 @@ public Void call(Object... args) throws Exception { assertEquals(HttpMethods.DELETE, request.getRequestMethod()); assertEquals(TEST_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); } } diff --git a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java index 8b3e75efd..6d7fb2a9e 100644 --- a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java +++ b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java @@ -127,6 +127,7 @@ public void testSuccessfulRequestWithHeadersAndBody() throws FirebaseException, assertEquals("v1", last.getHeaders().get("h1")); assertEquals("v2", last.getHeaders().get("h2")); assertEquals("v3", last.getHeaders().get("h3")); + assertEquals(SdkUtils.getMetricsHeader(), last.getHeaders().get("x-goog-api-client")); ByteArrayOutputStream out = new ByteArrayOutputStream(); last.getContent().writeTo(out); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index b75d9a3d6..805d409a0 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -389,6 +389,7 @@ private void checkRequestHeader(HttpRequest request) { HttpHeaders headers = request.getHeaders(); assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals(SdkUtils.getMetricsHeader(), headers.get("X-Goog-Api-Client")); } private void checkRequest( diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index 1a140680f..d0151bb94 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -41,6 +41,7 @@ import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; import java.io.ByteArrayOutputStream; @@ -447,6 +448,8 @@ private void checkTopicManagementRequestHeader( HttpRequest request, String expectedUrl) { assertEquals("POST", request.getRequestMethod()); assertEquals(expectedUrl, request.getUrl().toString()); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); + } private void checkExceptionFromHttpResponse( diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index 853074e65..4221adc6c 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -1124,6 +1124,7 @@ private void checkRequestHeader(int index, String expectedUrl, HttpMethod httpMe assertEquals(expectedUrl, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); assertEquals(CLIENT_VERSION, request.getHeaders().get("X-Client-Version")); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); } private void checkRequestPayload(Map expected) throws IOException { diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index 0a04809cf..edc52a19d 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -1185,6 +1185,7 @@ private void checkGetRequestHeader(HttpRequest request, String urlSuffix) { assertEquals(TEST_REMOTE_CONFIG_URL + urlSuffix, request.getUrl().toString()); HttpHeaders headers = request.getHeaders(); assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); assertEquals("gzip", headers.getAcceptEncoding()); } @@ -1197,6 +1198,7 @@ private void checkPutRequestHeader(HttpRequest request, String urlSuffix, String assertEquals(TEST_REMOTE_CONFIG_URL + urlSuffix, request.getUrl().toString()); HttpHeaders headers = request.getHeaders(); assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); assertEquals("gzip", headers.getAcceptEncoding()); assertEquals(ifMatch, headers.getIfMatch()); } @@ -1206,6 +1208,7 @@ private void checkPostRequestHeader(HttpRequest request, String urlSuffix) { assertEquals(TEST_REMOTE_CONFIG_URL + urlSuffix, request.getUrl().toString()); HttpHeaders headers = request.getHeaders(); assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); assertEquals("gzip", headers.getAcceptEncoding()); } From 724b9eee8d0541b2c984eee4b56eb7f6c1314944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:27:59 +0000 Subject: [PATCH 108/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.49.0 to 26.50.0 (#1040) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e0340a794..02e1b5967 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.49.0 + 26.50.0 pom import From 5e13dab4f1a602e057c944300468e1d3dc5a1b86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:32:50 +0000 Subject: [PATCH 109/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#1041) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 02e1b5967..ac9236309 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.10.1 + 3.11.1 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.10.1 + 3.11.1 attach-javadocs From 730f794611f2c737c8fe43701ca4d41e63b1fb62 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:28:53 -0500 Subject: [PATCH 110/219] fix: Catch `StorageException` in IT to temporarily unblock release (#1043) --- src/test/java/com/google/firebase/cloud/StorageClientIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/cloud/StorageClientIT.java b/src/test/java/com/google/firebase/cloud/StorageClientIT.java index 57fe58b82..4542e9735 100644 --- a/src/test/java/com/google/firebase/cloud/StorageClientIT.java +++ b/src/test/java/com/google/firebase/cloud/StorageClientIT.java @@ -23,6 +23,7 @@ import com.google.cloud.storage.Blob; import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.StorageException; import com.google.common.io.CharStreams; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; @@ -52,7 +53,7 @@ public void testCloudStorageNonExistingBucket() { try { storage.bucket("non-existing"); fail("No error thrown for non-existing bucket"); - } catch (IllegalArgumentException expected) { + } catch (IllegalArgumentException | StorageException expected) { // ignore } } From c47d657da544a71376ce3189513d2fea627b1941 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:21:10 -0500 Subject: [PATCH 111/219] [chore] Release 9.4.2 (#1042) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ac9236309..026f70b9d 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.4.1 + 9.4.2 jar firebase-admin From afea3c39b010e0424f1fe1e244ed7fe3eab9752b Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:43:30 -0500 Subject: [PATCH 112/219] chore: Adding delayed response message for holidays (#1049) * Adding delayed response message for holidays * fix date --- .github/ISSUE_TEMPLATE/bug_report.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 36eea6edb..59ba833f5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,11 @@ assignees: '' --- +--- +**Thank you for submitting your issue. We are operating at reduced capacity from Dec 23 2024 to Jan 6 2025. Please expect delayed responses. For more urgent requests please reach us via our support channels https://firebase.google.com/support** + +--- + ### [READ] Step 1: Are you in the right place? * For issues or feature requests related to __the code in this repository__ From d6d9b865650cec5457498a4b7ea620721fea8985 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Thu, 2 Jan 2025 08:52:09 -0800 Subject: [PATCH 113/219] Fix typo in FirebaseToken.java (#1051) Fixing typo noted in b/181365473. --- src/main/java/com/google/firebase/auth/FirebaseToken.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseToken.java b/src/main/java/com/google/firebase/auth/FirebaseToken.java index 835fa41c9..6950bf16f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseToken.java +++ b/src/main/java/com/google/firebase/auth/FirebaseToken.java @@ -37,12 +37,12 @@ public final class FirebaseToken { this.claims = ImmutableMap.copyOf(claims); } - /** Returns the Uid for the this token. */ + /** Returns the Uid for this token. */ public String getUid() { return (String) claims.get("sub"); } - /** Returns the tenant ID for the this token. */ + /** Returns the tenant ID for this token. */ public String getTenantId() { Map firebase = (Map) claims.get("firebase"); if (firebase == null) { @@ -51,7 +51,7 @@ public String getTenantId() { return (String) firebase.get("tenant"); } - /** Returns the Issuer for the this token. */ + /** Returns the Issuer for this token. */ public String getIssuer() { return (String) claims.get("iss"); } From 71d324b229ebf6de4e0f45799b3c9a9751ab80d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:34:42 +0000 Subject: [PATCH 114/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.50.0 to 26.52.0 (#1050) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 026f70b9d..823cf3cb8 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.50.0 + 26.52.0 pom import From 338e8642d2796308fc109d0389d6db556397ec90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:38:25 +0000 Subject: [PATCH 115/219] chore(deps): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1047) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 823cf3cb8..68051551b 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.5.1 + 3.5.2 From 2c203e2b4f2276f7ebc02e2cedeeaa619fad16ff Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:48:51 -0500 Subject: [PATCH 116/219] Revert "chore: Adding delayed response message for holidays (#1049)" (#1052) This reverts commit afea3c39b010e0424f1fe1e244ed7fe3eab9752b. --- .github/ISSUE_TEMPLATE/bug_report.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 59ba833f5..36eea6edb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,11 +7,6 @@ assignees: '' --- ---- -**Thank you for submitting your issue. We are operating at reduced capacity from Dec 23 2024 to Jan 6 2025. Please expect delayed responses. For more urgent requests please reach us via our support channels https://firebase.google.com/support** - ---- - ### [READ] Step 1: Are you in the right place? * For issues or feature requests related to __the code in this repository__ From 6b73abaa1a311042a60937a19c66696f0ca4a7b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:10:56 -0500 Subject: [PATCH 117/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#1054) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.11.1 to 3.11.2. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.11.1...maven-javadoc-plugin-3.11.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 68051551b..580cc8459 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.11.1 + 3.11.2 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.11.1 + 3.11.2 attach-javadocs From 4f4979dcccbf973b9821f9ec9d0414a6b5bcca37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:19:31 +0000 Subject: [PATCH 118/219] chore(deps): Bump netty.version from 4.1.114.Final to 4.1.116.Final (#1053) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 580cc8459..d265f63aa 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.114.Final + 4.1.116.Final From fca419ed0c28f6777d455c5419f011fc54855689 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:25:26 -0500 Subject: [PATCH 119/219] chore: Fix non-existing bucket test (#1056) --- src/test/java/com/google/firebase/cloud/StorageClientIT.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/google/firebase/cloud/StorageClientIT.java b/src/test/java/com/google/firebase/cloud/StorageClientIT.java index 4542e9735..acbaa8e46 100644 --- a/src/test/java/com/google/firebase/cloud/StorageClientIT.java +++ b/src/test/java/com/google/firebase/cloud/StorageClientIT.java @@ -23,7 +23,6 @@ import com.google.cloud.storage.Blob; import com.google.cloud.storage.Bucket; -import com.google.cloud.storage.StorageException; import com.google.common.io.CharStreams; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; @@ -51,9 +50,9 @@ public void testCloudStorageCustomBucket() { public void testCloudStorageNonExistingBucket() { StorageClient storage = StorageClient.getInstance(IntegrationTestUtils.ensureDefaultApp()); try { - storage.bucket("non-existing"); + storage.bucket("non.existing"); fail("No error thrown for non-existing bucket"); - } catch (IllegalArgumentException | StorageException expected) { + } catch (IllegalArgumentException expected) { // ignore } } From d6c5112d29d6e01c48c9c5646ec4ac13ea9c6fb2 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:12:06 -0500 Subject: [PATCH 120/219] [chore] Release 9.4.3 (#1057) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d265f63aa..b9ca166e4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.4.2 + 9.4.3 jar firebase-admin From 20e2b8bb6874d2cb03efecd6d4107499dc63b16c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:37:58 -0500 Subject: [PATCH 121/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.52.0 to 26.53.0 (#1060) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.52.0 to 26.53.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.52.0...v26.53.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b9ca166e4..2842b4c7a 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.52.0 + 26.53.0 pom import From 7bc8c12270526a85b4a17783fd4aaef1cc711a8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:30:14 +0000 Subject: [PATCH 122/219] chore(deps): Bump com.google.api-client:google-api-client-bom (#1063) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2842b4c7a..96dfe66d8 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.api-client google-api-client-bom - 2.7.0 + 2.7.2 pom import From af8eed95b3d49fc7b31d1462c86ecad924a2ffb7 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:56:52 -0500 Subject: [PATCH 123/219] fix: `ApiClientUtils.getDefaultTransport()` should return a cached instance to an `ApacheHttp2Transport` (#1059) * fix: `ApiClientUtils.getDefaultTransport()` should return cached instance to an `ApacheHttp2Transport` * fix: lint * reduce timeout time * fix: use new transport to trigger writeTimeout --- .../firebase/internal/ApiClientUtils.java | 6 +++++- .../internal/ApacheHttp2TransportIT.java | 21 +++++++++++++++++++ .../firebase/internal/ApiClientUtilsTest.java | 9 ++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 339726105..9d501fae3 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -88,6 +88,10 @@ public static JsonFactory getDefaultJsonFactory() { } public static HttpTransport getDefaultTransport() { - return new ApacheHttp2Transport(); + return TransportInstanceHolder.INSTANCE; + } + + private static class TransportInstanceHolder { + static final HttpTransport INSTANCE = new ApacheHttp2Transport(); } } diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 047e1de0b..0beb1b3b4 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -198,9 +198,12 @@ public void testReadTimeoutAuthorizedPost() throws FirebaseException { @Test(timeout = 10_000L) public void testWriteTimeoutAuthorizedGet() throws FirebaseException { + // Use a fresh transport so that writeTimeout triggers while waiting for the transport to + // be ready to receive data. app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setWriteTimeout(100) + .setHttpTransport(new ApacheHttp2Transport()) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); @@ -217,9 +220,12 @@ public void testWriteTimeoutAuthorizedGet() throws FirebaseException { @Test(timeout = 10_000L) public void testWriteTimeoutAuthorizedPost() throws FirebaseException { + // Use a fresh transport so that writeTimeout triggers while waiting for the transport to + // be ready to receive data. app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setWriteTimeout(100) + .setHttpTransport(new ApacheHttp2Transport()) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); @@ -292,6 +298,21 @@ public void testVerifyProxyIsRespected() { } } + @Test + public void testVerifyDefaultTransportReused() { + FirebaseOptions o1 = FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .build(); + + FirebaseOptions o2 = FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .build(); + + HttpTransport t1 = o1.getHttpTransport(); + HttpTransport t2 = o2.getHttpTransport(); + assertEquals(t1, t2); + } + private static ErrorHandlingHttpClient getHttpClient(boolean authorized, FirebaseApp app) { HttpRequestFactory requestFactory; diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java index f23c7a34d..e0e619f3c 100644 --- a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java +++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java @@ -25,6 +25,7 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; import com.google.api.client.http.HttpUnsuccessfulResponseHandler; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; @@ -128,4 +129,12 @@ public void disconnect() throws IOException { assertTrue(lowLevelResponse.isDisconnected()); } + + @Test + public void testVerifyDefaultTransportReused() { + HttpTransport t1 = ApiClientUtils.getDefaultTransport(); + HttpTransport t2 = ApiClientUtils.getDefaultTransport(); + assertEquals(t1, t2); + } + } From dcd8b9ed276dbb7d02a5e3725c2ec7e0d258c3d9 Mon Sep 17 00:00:00 2001 From: Yani Kapitanov Date: Tue, 28 Jan 2025 22:46:35 +0100 Subject: [PATCH 124/219] fix: use .equals() instead of == for strings (#1065) --- .../java/com/google/firebase/auth/AbstractFirebaseAuth.java | 5 +++-- src/main/java/com/google/firebase/auth/UserRecord.java | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index d1b4a513e..44fe9b7d2 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -600,9 +600,10 @@ private CallableOperation getUserByProviderUi // Although we don't really advertise it, we want to also handle // non-federated idps with this call. So if we detect one of them, we'll // reroute this request appropriately. - if (providerId == "phone") { + if ("phone".equals(providerId)) { return this.getUserByPhoneNumberOp(uid); - } else if (providerId == "email") { + } + if ("email".equals(providerId)) { return this.getUserByEmailOp(uid); } diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index ac962c52d..33c5fbe74 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -487,7 +487,7 @@ public UpdateRequest setPhoneNumber(@Nullable String phone) { // *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though // it might also be reasonable to relax this restriction and just unlink it. for (String dp : deleteProviderIterable) { - if (dp == "phone") { + if ("phone".equals(dp)) { throw new IllegalArgumentException( "Both UpdateRequest.setPhoneNumber(null) and " + "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a " @@ -601,7 +601,7 @@ public UpdateRequest setProvidersToUnlink(Iterable providerIds) { for (String id : providerIds) { checkArgument(!Strings.isNullOrEmpty(id), "providerIds must not be null or empty"); - if (id == "phone" && properties.containsKey("phoneNumber") + if ("phone".equals(id) && properties.containsKey("phoneNumber") && properties.get("phoneNumber") == null) { // If we've been told to unlink the phone provider both via setting phoneNumber to null // *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though From 7b360f2a01745186550c70bd830b50d7c6301dee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 19:35:17 +0000 Subject: [PATCH 125/219] chore(deps): Bump netty.version from 4.1.116.Final to 4.1.117.Final (#1067) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 96dfe66d8..6781d3c64 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.116.Final + 4.1.117.Final From c81173366c8491de64123ca848bd0d63ac062e08 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Mon, 3 Feb 2025 09:53:54 -0500 Subject: [PATCH 126/219] chore: Rearrange firebase admin pom deps (#1066) Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --- pom.xml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 6781d3c64..ed3fa87ad 100644 --- a/pom.xml +++ b/pom.xml @@ -389,18 +389,15 @@ pom import - - com.google.api-client - google-api-client-bom - 2.7.2 - pom - import - + + com.google.auth + google-auth-library-oauth2-http + com.google.api-client google-api-client @@ -417,10 +414,6 @@ com.google.api api-common - - com.google.auth - google-auth-library-oauth2-http - com.google.cloud google-cloud-storage From 6e505832d0fed0288440b8ad657bd568fd36247f Mon Sep 17 00:00:00 2001 From: vitvvv1 <36170346+vitvvv1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:08:37 +0200 Subject: [PATCH 127/219] feat(fcm): Support `proxy` field in FCM `AndroidNotification` (#1084) * add 'proxy' to AndroidNotification * fix: removed PROXY_UNSPECIFIED from AndroidNotification.Proxy * Update src/main/java/com/google/firebase/messaging/AndroidNotification.java fix comment AndroidNotification.setProxy Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --------- Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --- .../messaging/AndroidNotification.java | 29 ++++++++++++++++++- .../firebase/messaging/MessageTest.java | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 05e680910..b509a9843 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -112,6 +112,9 @@ public class AndroidNotification { @Key("notification_count") private final Integer notificationCount; + @Key("proxy") + private final String proxy; + private static final Map PRIORITY_MAP = ImmutableMap.builder() .put(Priority.MIN, "PRIORITY_MIN") @@ -179,6 +182,11 @@ private AndroidNotification(Builder builder) { checkArgument(builder.notificationCount >= 0, "notificationCount if specified must be zero or positive valued"); } + if (builder.proxy != null) { + this.proxy = builder.proxy.name(); + } else { + this.proxy = null; + } this.notificationCount = builder.notificationCount; } @@ -194,13 +202,19 @@ public String toString() { return PRIORITY_MAP.get(this); } } - + public enum Visibility { PRIVATE, PUBLIC, SECRET, } + public enum Proxy { + ALLOW, + DENY, + IF_PRIORITY_LOWERED + } + /** * Creates a new {@link AndroidNotification.Builder}. * @@ -237,6 +251,7 @@ public static class Builder { private LightSettings lightSettings; private Boolean defaultLightSettings; private Visibility visibility; + private Proxy proxy; private Builder() {} @@ -602,6 +617,18 @@ public Builder setNotificationCount(int notificationCount) { return this; } + /** + * Sets the proxy of this notification. + * + * @param proxy The proxy value, one of the values in {ALLOW, DENY, IF_PRIORITY_LOWERED} + * + * @return This builder. + */ + public Builder setProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + /** * Creates a new {@link AndroidNotification} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 992644b35..cea440cca 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -890,6 +890,7 @@ public void testExtendedAndroidNotificationParameters() throws IOException { .setDefaultLightSettings(false) .setVisibility(AndroidNotification.Visibility.PUBLIC) .setNotificationCount(10) + .setProxy(AndroidNotification.Proxy.DENY) .build()) .build()) .setTopic("test-topic") @@ -923,6 +924,7 @@ public void testExtendedAndroidNotificationParameters() throws IOException { .put("default_light_settings", false) .put("visibility", "public") .put("notification_count", new BigDecimal(10)) + .put("proxy", "DENY") .build()) .build(); assertJsonEquals(ImmutableMap.of( From ca953d82a950c1113e2e312f79e132f4d0daedc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:51:53 +0000 Subject: [PATCH 128/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.53.0 to 26.59.0 (#1087) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ed3fa87ad..0ae84529c 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.53.0 + 26.59.0 pom import From 1cf554545b6071e6e773b75398e32f4bf9700bd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:54:48 +0000 Subject: [PATCH 129/219] chore(deps): Bump org.apache.maven.plugins:maven-compiler-plugin (#1079) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0ae84529c..4235f2462 100644 --- a/pom.xml +++ b/pom.xml @@ -270,7 +270,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 1.8 1.8 From 9ac395de94be9a03611e23cacd391d4b592667cc Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:42:53 -0400 Subject: [PATCH 130/219] chore: Updated release GHA to have write permissions (#1088) --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca1f042c4..d6f744c69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,6 +78,8 @@ jobs: startsWith(github.event.pull_request.title, '[chore] Release ') runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout source for publish From e64da06a7e55a08476a6aba8e191f0481e954c0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 21:57:37 +0000 Subject: [PATCH 131/219] chore(deps): Bump netty.version from 4.1.117.Final to 4.1.118.Final (#1074) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4235f2462..81cce5d31 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.117.Final + 4.2.0.Final From f784855176d278f29b1d2e78a421c016bb07f994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 22:00:08 +0000 Subject: [PATCH 132/219] chore(deps): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1090) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 81cce5d31..67eb4119c 100644 --- a/pom.xml +++ b/pom.xml @@ -337,7 +337,7 @@ maven-failsafe-plugin - 3.5.2 + 3.5.3 From ac4a8ad337c37cc0b15f3d1154607681f6e4d9e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 22:08:04 +0000 Subject: [PATCH 133/219] chore(deps): Bump org.slf4j:slf4j-api from 2.0.16 to 2.0.17 (#1089) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 67eb4119c..c2290b86c 100644 --- a/pom.xml +++ b/pom.xml @@ -431,7 +431,7 @@ org.slf4j slf4j-api - 2.0.16 + 2.0.17 io.netty From c6970a5312e54e1df29efaec537b0572bdbaaff3 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 28 May 2025 16:26:57 -0400 Subject: [PATCH 134/219] feat(fcm): Add `liveActivityToken` to `ApnsConfig` (#1094) * feat: Add liveActivityToken to ApnsConfig Adds a new `liveActivityToken` field to the `ApnsConfig` class, allowing you to specify a Live Activity token for APNS messages. The changes include: - Addition of the `liveActivityToken` field in `ApnsConfig.java` and its associated builder. - Updates to unit tests in `MessageTest.java` to verify the correct serialization of the new field and to include it in existing test cases. A new test method `testApnsMessageWithOnlyLiveActivityToken` was added for specific testing. - Update to the integration test in `FirebaseMessagingIT.java` to include the `liveActivityToken` in a test message. An unrelated fix was also included in `MessageTest.java` to correctly use `new BigDecimal(long)` instead of `BigDecimal.valueOf(long)` for `notification_count`. * fix: Correct ApnsConfig liveActivityToken JSON key Corrects the `@Key` annotation for the `liveActivityToken` field in `ApnsConfig.java` from "live-activity-token" to "live_activity_token" to follow snake_case convention for JSON mapping. Unit tests in `MessageTest.java` have been updated to reflect this change in the expected JSON output. * remove unrelated changes --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --- .../google/firebase/messaging/ApnsConfig.java | 16 +++++++ .../messaging/FirebaseMessagingIT.java | 1 + .../firebase/messaging/MessageTest.java | 46 +++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index 0e5de8619..39092ac17 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -41,6 +41,9 @@ public class ApnsConfig { @Key("fcm_options") private final ApnsFcmOptions fcmOptions; + @Key("live_activity_token") + private final String liveActivityToken; + private ApnsConfig(Builder builder) { checkArgument(builder.aps != null, "aps must be specified"); checkArgument(!builder.customData.containsKey("aps"), @@ -51,6 +54,7 @@ private ApnsConfig(Builder builder) { .put("aps", builder.aps.getFields()) .build(); this.fcmOptions = builder.fcmOptions; + this.liveActivityToken = builder.liveActivityToken; } /** @@ -68,6 +72,7 @@ public static class Builder { private final Map customData = new HashMap<>(); private Aps aps; private ApnsFcmOptions fcmOptions; + private String liveActivityToken; private Builder() {} @@ -137,6 +142,17 @@ public Builder setFcmOptions(ApnsFcmOptions apnsFcmOptions) { return this; } + /** + * Sets the Live Activity token. + * + * @param liveActivityToken Live Activity token. + * @return This builder. + */ + public Builder setLiveActivityToken(String liveActivityToken) { + this.liveActivityToken = liveActivityToken; + return this; + } + /** * Creates a new {@link ApnsConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index d9375781c..91bfc5b9b 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -63,6 +63,7 @@ public void testSend() throws Exception { .setBody("Body") .build()) .build()) + .setLiveActivityToken("integration-test-live-activity-token") .build()) .setWebpushConfig(WebpushConfig.builder() .putHeader("X-Custom-Val", "Foo") diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index cea440cca..4ac1782ba 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -411,6 +411,7 @@ public void testApnsMessageWithPayload() throws IOException { .putCustomData("cd1", "cd-v1") .putAllCustomData(ImmutableMap.of("cd2", "cd-v2", "cd3", true)) .setAps(Aps.builder().build()) + .setLiveActivityToken("test-live-activity-token") .build()) .setTopic("test-topic") .build(); @@ -421,10 +422,11 @@ public void testApnsMessageWithPayload() throws IOException { .put("cd3", true) .put("aps", ImmutableMap.of()) .build(); - Map data = ImmutableMap.of( - "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), - "payload", payload - ); + Map data = ImmutableMap.builder() + .put("headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3")) + .put("payload", payload) + .put("live_activity_token", "test-live-activity-token") + .build(); assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", data), message); } @@ -442,6 +444,7 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { .setSound("test-sound") .setThreadId("test-thread-id") .build()) + .setLiveActivityToken("test-live-activity-token-aps") .build()) .setTopic("test-topic") .build(); @@ -459,7 +462,10 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { assertJsonEquals( ImmutableMap.of( "topic", "test-topic", - "apns", ImmutableMap.of("payload", payload)), + "apns", ImmutableMap.builder() + .put("payload", payload) + .put("live_activity_token", "test-live-activity-token-aps") + .build()), message); message = Message.builder() @@ -825,6 +831,7 @@ public void testImageInApnsNotification() throws IOException { .setApnsConfig( ApnsConfig.builder().setAps(Aps.builder().build()) .setFcmOptions(ApnsFcmOptions.builder().setImage(TEST_IMAGE_URL_APNS).build()) + .setLiveActivityToken("test-live-activity-token-image") .build()).build(); ImmutableMap notification = @@ -837,6 +844,7 @@ public void testImageInApnsNotification() throws IOException { ImmutableMap.builder() .put("fcm_options", ImmutableMap.of("image", TEST_IMAGE_URL_APNS)) .put("payload", ImmutableMap.of("aps", ImmutableMap.of())) + .put("live_activity_token", "test-live-activity-token-image") .build(); ImmutableMap expected = ImmutableMap.builder() @@ -847,6 +855,34 @@ public void testImageInApnsNotification() throws IOException { assertJsonEquals(expected, message); } + @Test + public void testApnsMessageWithOnlyLiveActivityToken() throws IOException { + Message message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder().build()) + .setLiveActivityToken("only-live-activity") + .build()) + .setTopic("test-topic") + .build(); + Map expectedApns = ImmutableMap.builder() + .put("live_activity_token", "only-live-activity") + .put("payload", ImmutableMap.of("aps", ImmutableMap.of())) + .build(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", expectedApns), message); + + // Test without live activity token + message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder().build()) + .build()) + .setTopic("test-topic") + .build(); + expectedApns = ImmutableMap.builder() + .put("payload", ImmutableMap.of("aps", ImmutableMap.of())) + .build(); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "apns", expectedApns), message); + } + @Test public void testInvalidColorInAndroidNotificationLightSettings() { try { From 4f4476cf06c12bcbe1d831750c060e242d0dcef3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:57:28 +0000 Subject: [PATCH 135/219] chore(deps): Bump org.jacoco:jacoco-maven-plugin from 0.8.12 to 0.8.13 (#1096) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c2290b86c..b12609f6a 100644 --- a/pom.xml +++ b/pom.xml @@ -189,7 +189,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 + 0.8.13 pre-unit-test From 817dba21bef7834efce7024dbd2234531345c0a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 21:01:42 +0000 Subject: [PATCH 136/219] chore(deps): Bump netty.version from 4.2.0.Final to 4.2.1.Final (#1097) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b12609f6a..0310de19c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.0.Final + 4.2.1.Final From 4f73761203316c3d84b9993aebb960c784f561bb Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 29 May 2025 13:20:46 -0400 Subject: [PATCH 137/219] [chore] Release 9.5.0 (#1099) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0310de19c..96c5ec223 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.4.3 + 9.5.0 jar firebase-admin From ac183c020a2fdbe253bea528aa685bfffde01f84 Mon Sep 17 00:00:00 2001 From: joefspiro <97258781+joefspiro@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:39:15 -0400 Subject: [PATCH 138/219] Adds sendEach based snippets for FCM. (#1104) * Adds sendEach based snippets for FCM. * Corrects getting the messaging instance. --- .../snippets/FirebaseMessagingSnippets.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index 4a4a25472..b17a5d58a 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -149,6 +149,36 @@ public void sendAll() throws FirebaseMessagingException { // [END send_all] } + public void sendEach() throws FirebaseMessagingException { + String registrationToken = "YOUR_REGISTRATION_TOKEN"; + + // [START send_each] + // Create a list containing up to 500 messages. + List messages = Arrays.asList( + Message.builder() + .setNotification(Notification.builder() + .setTitle("Price drop") + .setBody("5% off all electronics") + .build()) + .setToken(registrationToken) + .build(), + // ... + Message.builder() + .setNotification(Notification.builder() + .setTitle("Price drop") + .setBody("2% off all books") + .build()) + .setTopic("readers-club") + .build() + ); + + BatchResponse response = FirebaseMessaging.getInstance().sendEach(messages); + // See the BatchResponse reference documentation + // for the contents of response. + System.out.println(response.getSuccessCount() + " messages were sent successfully"); + // [END send_each] + } + public void sendMulticast() throws FirebaseMessagingException { // [START send_multicast] // Create a list containing up to 500 registration tokens. @@ -201,6 +231,36 @@ public void sendMulticastAndHandleErrors() throws FirebaseMessagingException { // [END send_multicast_error] } + public void sendEachForMulticastAndHandleErrors() throws FirebaseMessagingException { + // [START send_each_for_multicast_error] + // These registration tokens come from the client FCM SDKs. + List registrationTokens = Arrays.asList( + "YOUR_REGISTRATION_TOKEN_1", + // ... + "YOUR_REGISTRATION_TOKEN_n" + ); + + MulticastMessage message = MulticastMessage.builder() + .putData("score", "850") + .putData("time", "2:45") + .addAllTokens(registrationTokens) + .build(); + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message); + if (response.getFailureCount() > 0) { + List responses = response.getResponses(); + List failedTokens = new ArrayList<>(); + for (int i = 0; i < responses.size(); i++) { + if (!responses.get(i).isSuccessful()) { + // The order of responses corresponds to the order of the registration tokens. + failedTokens.add(registrationTokens.get(i)); + } + } + + System.out.println("List of tokens that caused failures: " + failedTokens); + } + // [END send_each_for_multicast_error] + } + public Message androidMessage() { // [START android_message] Message message = Message.builder() From 82b09a6bfd6d8f27c1991f31623f398439f311c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:34:41 +0000 Subject: [PATCH 139/219] chore(deps): Bump org.apache.maven.plugins:maven-project-info-reports-plugin (#1102) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 96c5ec223..15bf121ba 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ maven-project-info-reports-plugin - 3.8.0 + 3.9.0 From 316082b9f3ccf5b97a053a581e79ca8f911c3e96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:37:19 -0400 Subject: [PATCH 140/219] chore(deps): Bump org.apache.maven.plugins:maven-surefire-plugin (#1101) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.2 to 3.5.3. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.2...surefire-3.5.3) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 15bf121ba..cec632146 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,7 @@ maven-surefire-plugin - 3.5.2 + 3.5.3 ${skipUTs} From 4aba7f5be04ca54418caa9393621989ee585d549 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:05:59 +0000 Subject: [PATCH 141/219] chore(deps): Bump netty.version from 4.2.1.Final to 4.2.2.Final (#1105) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cec632146..f2ae9d1b4 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.1.Final + 4.2.2.Final From 7611f56e8ff06b3f0871f551f6973916bccc0404 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:11:26 +0000 Subject: [PATCH 142/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.59.0 to 26.62.0 (#1106) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f2ae9d1b4..60b450481 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.59.0 + 26.62.0 pom import From 46117439d7b81beb8bc1b61b6bb4d0068affe356 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:57:19 +0000 Subject: [PATCH 143/219] chore(deps): Bump org.codehaus.mojo:exec-maven-plugin (#1109) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 60b450481..8b1cef4b3 100644 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.codehaus.mojo exec-maven-plugin - 3.5.0 + 3.5.1 test From 6b105cc065f2b0a8b863082900a14648f2921543 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:48:51 -0400 Subject: [PATCH 144/219] chore: Update tests for getting and deleting provider uid via phone number and email. (#1068) --- .../firebase/auth/FirebaseUserManagerTest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index c8c733ce4..9369eae8d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -402,9 +402,11 @@ public void testGetUserByProviderUidWithPhone() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("getUser.json")); UserRecord userRecord = FirebaseAuth.getInstance() - .getUserByProviderUidAsync("phone", "+1234567890").get(); + .getUserByProviderUidAsync(new String("phone"), "+1234567890").get(); checkUserRecord(userRecord); checkRequestHeaders(interceptor); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(ImmutableList.of("+1234567890"), parsed.get("phoneNumber")); } @Test @@ -412,9 +414,11 @@ public void testGetUserByProviderUidWithEmail() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("getUser.json")); UserRecord userRecord = FirebaseAuth.getInstance() - .getUserByProviderUidAsync("email", "testuser@example.com").get(); + .getUserByProviderUidAsync(new String("email"), "testuser@example.com").get(); checkUserRecord(userRecord); checkRequestHeaders(interceptor); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(ImmutableList.of("testuser@example.com"), parsed.get("email")); } @Test @@ -1248,11 +1252,12 @@ public void testDeleteProviderAndPhone() { @Test public void testDoubleDeletePhoneProvider() throws Exception { + String providerId = new String("phone"); UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("uid") .setPhoneNumber(null); try { - update.setProvidersToUnlink(ImmutableList.of("phone")); + update.setProvidersToUnlink(ImmutableList.of(providerId)); fail("No error thrown for double delete phone provider"); } catch (IllegalArgumentException expected) { } @@ -1260,8 +1265,9 @@ public void testDoubleDeletePhoneProvider() throws Exception { @Test public void testDoubleDeletePhoneProviderReverseOrder() throws Exception { + String providerId = new String("phone"); UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("uid") - .setProvidersToUnlink(ImmutableList.of("phone")); + .setProvidersToUnlink(ImmutableList.of(providerId)); try { update.setPhoneNumber(null); From 813585c970b27f9af718ee00b0e8a918a04459d2 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:14:54 -0400 Subject: [PATCH 145/219] chore: fixed broken google cloud doc reference links (#1112) --- .../com/google/firebase/cloud/FirestoreClient.java | 10 +++++----- .../java/com/google/firebase/cloud/StorageClient.java | 4 ++-- .../firebase/snippets/FirebaseStorageSnippets.java | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index e55d5c7e9..682c39f07 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -19,7 +19,7 @@ /** * {@code FirestoreClient} provides access to Google Cloud Firestore. Use this API to obtain a - * {@code Firestore} + * {@code Firestore} * instance, which provides methods for updating and querying data in Firestore. * *

A Google Cloud project ID is required to access Firestore. FirestoreClient determines the @@ -60,7 +60,7 @@ private FirestoreClient(FirebaseApp app, String databaseId) { * same instance for all invocations. The Firestore instance and all references obtained from it * becomes unusable, once the default app is deleted. * - * @return A non-null {@code Firestore} + * @return A non-null {@code Firestore} * instance. */ @NonNull @@ -74,7 +74,7 @@ public static Firestore getFirestore() { * obtained from it becomes unusable, once the specified app is deleted. * * @param app A non-null {@link FirebaseApp}. - * @return A non-null {@code Firestore} + * @return A non-null {@code Firestore} * instance. */ @NonNull @@ -90,7 +90,7 @@ public static Firestore getFirestore(FirebaseApp app) { * * @param app A non-null {@link FirebaseApp}. * @param database - The name of database. - * @return A non-null {@code Firestore} + * @return A non-null {@code Firestore} * instance. */ @NonNull @@ -104,7 +104,7 @@ public static Firestore getFirestore(FirebaseApp app, String database) { * references obtained from it becomes unusable, once the default app is deleted. * * @param database - The name of database. - * @return A non-null {@code Firestore} + * @return A non-null {@code Firestore} * instance. */ @NonNull diff --git a/src/main/java/com/google/firebase/cloud/StorageClient.java b/src/main/java/com/google/firebase/cloud/StorageClient.java index 955abd7e2..172279990 100644 --- a/src/main/java/com/google/firebase/cloud/StorageClient.java +++ b/src/main/java/com/google/firebase/cloud/StorageClient.java @@ -71,7 +71,7 @@ public static synchronized StorageClient getInstance(FirebaseApp app) { * configured via {@link com.google.firebase.FirebaseOptions} when initializing the app. If * no bucket was configured via options, this method throws an exception. * - * @return a cloud storage {@code Bucket} + * @return a cloud storage {@code Bucket} * instance. * @throws IllegalArgumentException If no bucket is configured via FirebaseOptions, * or if the bucket does not exist. @@ -84,7 +84,7 @@ public Bucket bucket() { * Returns a cloud storage Bucket instance for the specified bucket name. * * @param name a non-null, non-empty bucket name. - * @return a cloud storage {@code Bucket} + * @return a cloud storage {@code Bucket} * instance. * @throws IllegalArgumentException If the bucket name is null, empty, or if the specified * bucket does not exist. diff --git a/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java index 706cc1b16..0c41dbd6a 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java @@ -42,7 +42,7 @@ public void initializeAppForStorage() throws IOException { Bucket bucket = StorageClient.getInstance().bucket(); // 'bucket' is an object defined in the google-cloud-storage Java library. - // See http://googlecloudplatform.github.io/google-cloud-java/latest/apidocs/com/google/cloud/storage/Bucket.html + // See https://cloud.google.com/java/docs/reference/google-cloud-storage/latest/com.google.cloud.storage.Bucket // for more details. // [END init_admin_sdk_for_storage] System.out.println("Retrieved bucket: " + bucket.getName()); From df5cd667809e3eb33bdfb5a356c33adb7d93cd3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:34:48 +0000 Subject: [PATCH 146/219] chore(deps): Bump org.apache.maven.plugins:maven-gpg-plugin (#1111) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8b1cef4b3..fac974972 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ maven-gpg-plugin - 3.2.7 + 3.2.8 sign-artifacts From 51258b524a7097cd183497e634e0cbfd20ee52f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:41:55 +0000 Subject: [PATCH 147/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.62.0 to 26.63.0 (#1110) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fac974972..d04209790 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.62.0 + 26.63.0 pom import From 91c1400aa24995e61d22357cc26aebd0d48594e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:25:57 +0000 Subject: [PATCH 148/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.63.0 to 26.65.0 (#1116) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d04209790..3861679f7 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.63.0 + 26.65.0 pom import From 212ecea4bcccf4f6a9df42d21f70f66ebefe809b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:29:22 +0000 Subject: [PATCH 149/219] chore(deps): Bump netty.version from 4.2.2.Final to 4.2.3.Final (#1113) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3861679f7..8729a2b34 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.2.Final + 4.2.3.Final From e406aca8560f8509e3021075a66b34ac5167a667 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Tue, 2 Sep 2025 08:05:15 -0700 Subject: [PATCH 150/219] feat(auth): Firebase Auth add link domain to actionCodeSetting (#1115) * add link domain to actionCodeSetting * fix: Correct deprecation tag * fix(auth): Fix doc string and use correct error message --------- Co-authored-by: jonathanedey Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --- .../firebase/auth/ActionCodeSettings.java | 20 +++++++++++++++++++ .../google/firebase/auth/AuthErrorCode.java | 5 +++++ .../auth/internal/AuthErrorHandler.java | 7 +++++++ .../firebase/auth/ActionCodeSettingsTest.java | 2 ++ 4 files changed, 34 insertions(+) diff --git a/src/main/java/com/google/firebase/auth/ActionCodeSettings.java b/src/main/java/com/google/firebase/auth/ActionCodeSettings.java index 0b102b7a3..8ffccd542 100644 --- a/src/main/java/com/google/firebase/auth/ActionCodeSettings.java +++ b/src/main/java/com/google/firebase/auth/ActionCodeSettings.java @@ -51,6 +51,9 @@ private ActionCodeSettings(Builder builder) { if (!Strings.isNullOrEmpty(builder.dynamicLinkDomain)) { properties.put("dynamicLinkDomain", builder.dynamicLinkDomain); } + if (!Strings.isNullOrEmpty(builder.linkDomain)) { + properties.put("linkDomain", builder.linkDomain); + } if (!Strings.isNullOrEmpty(builder.iosBundleId)) { properties.put("iOSBundleId", builder.iosBundleId); } @@ -84,6 +87,7 @@ public static final class Builder { private String url; private boolean handleCodeInApp; private String dynamicLinkDomain; + private String linkDomain; private String iosBundleId; private String androidPackageName; private String androidMinimumVersion; @@ -135,12 +139,28 @@ public Builder setHandleCodeInApp(boolean handleCodeInApp) { * * @param dynamicLinkDomain Firebase Dynamic Link domain string. * @return This builder. + * @deprecated Use {@link #setLinkDomain(String)} instead. */ + @Deprecated public Builder setDynamicLinkDomain(String dynamicLinkDomain) { this.dynamicLinkDomain = dynamicLinkDomain; return this; } + /** + * Sets the link domain to use for the current link if it is to be opened using + * {@code handleCodeInApp}, as multiple link domains can be configured per project. This + * setting provides the ability to explicitly choose one. If none is provided, the default + * Firebase Hosting domain will be used. + * + * @param linkDomain Link domain string. + * @return This builder. + */ + public Builder setLinkDomain(String linkDomain) { + this.linkDomain = linkDomain; + return this; + } + /** * Sets the bundle ID of the iOS app where the link should be handled if the * application is already installed on the device. diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java index 90b5da1a2..9f7ecebf1 100644 --- a/src/main/java/com/google/firebase/auth/AuthErrorCode.java +++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java @@ -58,6 +58,11 @@ public enum AuthErrorCode { */ INVALID_DYNAMIC_LINK_DOMAIN, + /** + * The provided hosting link domain is not configured or authorized for the current project. + */ + INVALID_HOSTING_LINK_DOMAIN, + /** * The specified ID token is invalid. */ diff --git a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java index 4b9523c83..346bd6a84 100644 --- a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java +++ b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java @@ -73,6 +73,13 @@ final class AuthErrorHandler extends AbstractHttpErrorHandler Date: Tue, 2 Sep 2025 15:23:53 +0000 Subject: [PATCH 151/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.65.0 to 26.66.0 (#1119) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8729a2b34..aa2c74f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -385,7 +385,7 @@ com.google.cloud libraries-bom - 26.65.0 + 26.67.0 pom import From ae5537ebe65536e73e6d93e0a42164205dedf9e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:26:47 +0000 Subject: [PATCH 152/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#1118) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index aa2c74f5c..1a7c778f2 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ maven-javadoc-plugin - 3.11.2 + 3.11.3 site @@ -301,7 +301,7 @@ maven-javadoc-plugin - 3.11.2 + 3.11.3 attach-javadocs From 0406ed57ed17cd83030a50b43022622b684c2f66 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:55:56 -0400 Subject: [PATCH 153/219] fix(auth): Fixed error code mapping for Auth errors (#1121) --- .../auth/internal/AuthErrorHandler.java | 4 +-- .../google/firebase/auth/FirebaseAuthIT.java | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java index 346bd6a84..cd570c64d 100644 --- a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java +++ b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java @@ -187,7 +187,7 @@ String buildMessage(AuthServiceErrorResponse response) { /** * JSON data binding for JSON error messages sent by Google identity toolkit service. These - * error messages take the form `{"error": {"message": "CODE: OPTIONAL DETAILS"}}`. + * error messages take the form `{"error": {"message": "CODE : OPTIONAL DETAILS"}}`. */ private static class AuthServiceErrorResponse { @@ -203,7 +203,7 @@ public String getCode() { int separator = message.indexOf(':'); if (separator != -1) { - return message.substring(0, separator); + return message.substring(0, separator).trim(); } return message; diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 5862450ae..41ccc5c20 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -82,6 +82,7 @@ public class FirebaseAuthIT { private static final JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); private static final HttpTransport transport = ApiClientUtils.getDefaultTransport(); private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3"; + private static final String INVALID_ACTION_LINK_CONTINUE_URL = "http://www.localhost/?a=1&b=2#c=3"; private static final FirebaseAuth auth = FirebaseAuth.getInstance( IntegrationTestUtils.ensureDefaultApp()); @@ -868,6 +869,31 @@ public void testGenerateSignInWithEmailLink() throws Exception { assertTrue(auth.getUser(user.getUid()).isEmailVerified()); } + @Test + public void testAuthErrorCodeParse() throws Exception { + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) + .setEmailVerified(false) + .setPassword("password")); + try { + auth.generateSignInWithEmailLink(user.getEmail(), ActionCodeSettings.builder() + .setUrl(INVALID_ACTION_LINK_CONTINUE_URL) + .build()); + fail("No error thrown for invlaid custom hosting domain"); + } catch (FirebaseAuthException e) { + assertEquals( + "The domain of the continue URL is not whitelisted (UNAUTHORIZED_DOMAIN): Domain not " + + "allowlisted by project", + e.getMessage()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.UNAUTHORIZED_CONTINUE_URL, e.getAuthErrorCode()); + } + } + @Test public void testOidcProviderConfigLifecycle() throws Exception { // Create provider config From 8fe1544372faa884058639d53a36744a04777832 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:09:36 +0000 Subject: [PATCH 154/219] chore(deps): Bump netty.version from 4.2.3.Final to 4.2.4.Final (#1123) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1a7c778f2..28a127857 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.3.Final + 4.2.4.Final From a9ff3f7332a731e5d63d6f2d33c7ddb0372583bb Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:06:58 -0400 Subject: [PATCH 155/219] [chore] Release 9.6.0 (#1125) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28a127857..b353743ee 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.5.0 + 9.6.0 jar firebase-admin From 353fc53740244ffc5b3f3b496d4b6d351c3f805a Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:18:16 -0400 Subject: [PATCH 156/219] Revert "[chore] Release 9.6.0 (#1125)" (#1126) This reverts commit a9ff3f7332a731e5d63d6f2d33c7ddb0372583bb. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b353743ee..28a127857 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.6.0 + 9.5.0 jar firebase-admin From 1b5cf37f068fdb9778de54c44744e698235cfc5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:39:39 -0400 Subject: [PATCH 157/219] chore(deps): Bump io.netty:netty-codec-http (#1127) Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.2.4.Final to 4.2.5.Final. - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.5.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.5.Final dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28a127857..990e4d6dc 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.4.Final + 4.2.5.Final From 304a274fadf44792503e50bbbdd9774acc0e6026 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 5 Sep 2025 12:25:44 -0400 Subject: [PATCH 158/219] feat: Migrate to Central Publisher Portal (#1128) Updates the release process to use the new Central Publisher Portal for deploying artifacts to Maven Central. - Replaces the nexus-staging-maven-plugin with the central-publishing-maven-plugin. - Updates the distributionManagement repository URL in pom.xml. - Modifies the publish script to use the central-publishing:publish goal. - Updates the release workflow to use token-based authentication secrets (CENTRAL_USERNAME, CENTRAL_TOKEN). - Adjusts settings.xml to use the new authentication tokens. --- .github/resources/settings.xml | 6 +++--- .github/scripts/publish_artifacts.sh | 2 +- .github/workflows/release.yml | 4 ++-- pom.xml | 19 ++++++------------- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml index f3bc754e2..da1d3057d 100644 --- a/.github/resources/settings.xml +++ b/.github/resources/settings.xml @@ -7,9 +7,9 @@ - ossrh - ${env.NEXUS_OSSRH_USERNAME} - ${env.NEXUS_OSSRH_PASSWORD} + central + ${env.CENTRAL_USERNAME} + ${env.CENTRAL_TOKEN} diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh index cd1a5b75c..8e20af88f 100755 --- a/.github/scripts/publish_artifacts.sh +++ b/.github/scripts/publish_artifacts.sh @@ -26,7 +26,7 @@ gpg --import --no-tty --batch --yes firebase.asc # 1. Compiles the source (compile phase) # 2. Packages the artifacts - src, bin, javadocs (package phase) # 3. Signs the artifacts (verify phase) -# 4. Publishes artifacts via Nexus (deploy phase) +# 4. Publishes artifacts via Central Publisher Portal (deploy phase) mvn -B clean deploy \ -Dcheckstyle.skip \ -DskipTests \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6f744c69..86774e50c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,8 +100,8 @@ jobs: env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} - NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }} # See: https://cli.github.com/manual/gh_release_create - name: Create release tag diff --git a/pom.xml b/pom.xml index 990e4d6dc..ff3a085f3 100644 --- a/pom.xml +++ b/pom.xml @@ -69,13 +69,6 @@ HEAD - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - devsite-apidocs @@ -350,14 +343,14 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - ossrh - https://oss.sonatype.org/ - true + central + true + published From 450fe3cd979d60d067a8d89cbc2530f703ac77a5 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:03:19 -0400 Subject: [PATCH 159/219] [chore] Release 9.6.0 Take 2 (#1130) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ff3a085f3..91c203218 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.5.0 + 9.6.0 jar firebase-admin From 026db9fec98ca1f66a9b16be0a37ac4e127a024e Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:48:26 +0530 Subject: [PATCH 160/219] feat(rc): Add support for Server-Side Remote Config (#1122) * Implementation for Fetching and Caching Server Side Remote Config (#1107)009 * Implementation for Fetching and Caching Server Side Remote Config * implementation of fetch , cache and load of template --------- Co-authored-by: Varun Rathore * Implement custom signal targeting for server side RC (#1108) Co-authored-by: Athira M * Implement percent evaluation for server side RC (#1114) * [feat] Implement percent evaluation for server side RC * Ssrc bugbash fix (#1117) * Handle empty context * fix issue related to update time * fix string equality * fix textcase * Fix lint errors * Add unit tests * fix for [438426692](getDouble() logs a malformed warning on type conversion failure) Using getDouble on a string parameter value, returns the appropriate default static value but logs a warning which looks incorrect ("%s" in the warning message?). * Update ServerTemplateResponse.java to fix b/438607881 In the server template builder flow using cached template, evaluation using custom signals is not working as intended. * Update getServerRemoteConfig.json to fix b/438607881 * Update getServerTemplateData.json to fix b/438607881 * fix for bugs * Resolve comment related to revert of ServerVersion Class * remove serverVersion * Resolve comments related to Evaluator * fix indentation * fix indentation * fix indentations * fix multi line indent * fix multi line indents * Update ConditionEvaluator.java * Update ConditionEvaluator.java --------- Co-authored-by: Athira M Co-authored-by: Varun Rathore * Create ParameterValueTest.java * Fix typo errors * Change return type and cache regex * Addressed comment to make cache atomic * Trigger CI --------- Co-authored-by: Varun Rathore Co-authored-by: Athira M Co-authored-by: Athira M --- .../firebase/remoteconfig/AndCondition.java | 61 ++ .../remoteconfig/ConditionEvaluator.java | 346 ++++++++ .../remoteconfig/CustomSignalCondition.java | 140 +++ .../remoteconfig/CustomSignalOperator.java | 54 ++ .../remoteconfig/FirebaseRemoteConfig.java | 68 ++ .../FirebaseRemoteConfigClient.java | 3 + .../FirebaseRemoteConfigClientImpl.java | 24 + .../firebase/remoteconfig/KeysAndValues.java | 135 +++ .../remoteconfig/MicroPercentRange.java | 46 + .../firebase/remoteconfig/OneOfCondition.java | 140 +++ .../firebase/remoteconfig/OrCondition.java | 58 ++ .../remoteconfig/PercentCondition.java | 163 ++++ .../PercentConditionOperator.java | 55 ++ .../remoteconfig/ServerCondition.java | 90 ++ .../firebase/remoteconfig/ServerConfig.java | 102 +++ .../firebase/remoteconfig/ServerTemplate.java | 45 + .../remoteconfig/ServerTemplateData.java | 215 +++++ .../remoteconfig/ServerTemplateImpl.java | 195 ++++ .../google/firebase/remoteconfig/Value.java | 135 +++ .../firebase/remoteconfig/ValueSource.java | 31 + .../internal/ServerTemplateResponse.java | 322 +++++++ .../remoteconfig/ConditionEvaluatorTest.java | 832 ++++++++++++++++++ .../FirebaseRemoteConfigClientImplTest.java | 823 ++++++++++++----- .../FirebaseRemoteConfigTest.java | 157 +++- .../remoteconfig/MockRemoteConfigClient.java | 25 +- .../remoteconfig/ServerConditionTest.java | 218 +++++ .../remoteconfig/ServerTemplateImplTest.java | 421 +++++++++ .../firebase/remoteconfig/ValueTest.java | 104 +++ .../firebase/remoteconfig/VersionTest.java | 12 +- src/test/resources/getServerRemoteConfig.json | 142 +++ src/test/resources/getServerTemplateData.json | 181 ++++ 31 files changed, 5094 insertions(+), 249 deletions(-) create mode 100644 src/main/java/com/google/firebase/remoteconfig/AndCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/OrCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/PercentCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerConfig.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Value.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ValueSource.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ValueTest.java create mode 100644 src/test/resources/getServerRemoteConfig.json create mode 100644 src/test/resources/getServerTemplateData.json diff --git a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java new file mode 100644 index 000000000..ec118cef2 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.AndConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; + +import java.util.List; +import java.util.stream.Collectors; + +final class AndCondition { + private final ImmutableList conditions; + + AndCondition(@NonNull List conditions) { + checkNotNull(conditions, "List of conditions for AND operation must not be null."); + checkArgument(!conditions.isEmpty(), + "List of conditions for AND operation must not be empty."); + this.conditions = ImmutableList.copyOf(conditions); + } + + AndCondition(AndConditionResponse andConditionResponse) { + List conditionList = andConditionResponse.getConditions(); + checkNotNull(conditionList, "List of conditions for AND operation must not be null."); + checkArgument(!conditionList.isEmpty(), + "List of conditions for AND operation must not be empty"); + this.conditions = conditionList.stream() + .map(OneOfCondition::new) + .collect(ImmutableList.toImmutableList()); + } + + @NonNull + ImmutableList getConditions() { + return conditions; + } + + AndConditionResponse toAndConditionResponse() { + return new AndConditionResponse() + .setConditions(this.conditions.stream() + .map(OneOfCondition::toOneOfConditionResponse) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java new file mode 100644 index 000000000..63718b32b --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -0,0 +1,346 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.IntPredicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ConditionEvaluator { + private static final int MAX_CONDITION_RECURSION_DEPTH = 10; + private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class); + private static final BigInteger MICRO_PERCENT_MODULO = BigInteger.valueOf(100_000_000L); + private static final Pattern SEMVER_PATTERN = Pattern.compile("^[0-9]+(?:\\.[0-9]+){0,4}$"); + + /** + * Evaluates server conditions and assigns a boolean value to each condition. + * + * @param conditions List of conditions which are to be evaluated. + * @param context A map with additional metadata used during evaluation. + * @return A map of condition to evaluated value. + */ + @NonNull + Map evaluateConditions( + @NonNull List conditions, @Nullable KeysAndValues context) { + checkNotNull(conditions, "List of conditions must not be null."); + checkArgument(!conditions.isEmpty(), "List of conditions must not be empty."); + if (context == null || conditions.isEmpty()) { + return ImmutableMap.of(); + } + KeysAndValues evaluationContext = + context != null ? context : new KeysAndValues.Builder().build(); + + Map evaluatedConditions = + conditions.stream() + .collect( + toImmutableMap( + ServerCondition::getName, + condition -> + evaluateCondition( + condition.getCondition(), evaluationContext, /* nestingLevel= */ 0))); + + return evaluatedConditions; + } + + private boolean evaluateCondition( + OneOfCondition condition, KeysAndValues context, int nestingLevel) { + if (nestingLevel > MAX_CONDITION_RECURSION_DEPTH) { + logger.warn("Maximum condition recursion depth exceeded."); + return false; + } + + if (condition.getOrCondition() != null) { + return evaluateOrCondition(condition.getOrCondition(), context, nestingLevel + 1); + } else if (condition.getAndCondition() != null) { + return evaluateAndCondition(condition.getAndCondition(), context, nestingLevel + 1); + } else if (condition.isTrue() != null) { + return true; + } else if (condition.isFalse() != null) { + return false; + } else if (condition.getCustomSignal() != null) { + return evaluateCustomSignalCondition(condition.getCustomSignal(), context); + } else if (condition.getPercent() != null) { + return evaluatePercentCondition(condition.getPercent(), context); + } + logger.atWarn().log("Received invalid condition for evaluation."); + return false; + } + + private boolean evaluateOrCondition( + OrCondition condition, KeysAndValues context, int nestingLevel) { + return condition.getConditions().stream() + .anyMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1)); + } + + private boolean evaluateAndCondition( + AndCondition condition, KeysAndValues context, int nestingLevel) { + return condition.getConditions().stream() + .allMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1)); + } + + private boolean evaluateCustomSignalCondition( + CustomSignalCondition condition, KeysAndValues context) { + CustomSignalOperator customSignalOperator = condition.getCustomSignalOperator(); + String customSignalKey = condition.getCustomSignalKey(); + ImmutableList targetCustomSignalValues = + ImmutableList.copyOf(condition.getTargetCustomSignalValues()); + + if (targetCustomSignalValues.isEmpty()) { + logger.warn( + String.format( + "Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s", + customSignalOperator, customSignalKey, targetCustomSignalValues)); + return false; + } + + String customSignalValue = context.get(customSignalKey); + if (customSignalValue == null) { + return false; + } + + switch (customSignalOperator) { + // String operations. + case STRING_CONTAINS: + return compareStrings( + targetCustomSignalValues, + customSignalValue, + (customSignal, targetSignal) -> customSignal.contains(targetSignal)); + case STRING_DOES_NOT_CONTAIN: + return !compareStrings( + targetCustomSignalValues, + customSignalValue, + (customSignal, targetSignal) -> customSignal.contains(targetSignal)); + case STRING_EXACTLY_MATCHES: + return compareStrings( + targetCustomSignalValues, + customSignalValue, + (customSignal, targetSignal) -> customSignal.equals(targetSignal)); + case STRING_CONTAINS_REGEX: + return compareStrings( + targetCustomSignalValues, + customSignalValue, + (customSignal, targetSignal) -> compareStringRegex(customSignal, targetSignal)); + + // Numeric operations. + case NUMERIC_LESS_THAN: + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result < 0); + case NUMERIC_LESS_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result <= 0); + case NUMERIC_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result == 0); + case NUMERIC_NOT_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result != 0); + case NUMERIC_GREATER_THAN: + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result > 0); + case NUMERIC_GREATER_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result >= 0); + + // Semantic operations. + case SEMANTIC_VERSION_EQUAL: + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result == 0); + case SEMANTIC_VERSION_GREATER_EQUAL: + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result >= 0); + case SEMANTIC_VERSION_GREATER_THAN: + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result > 0); + case SEMANTIC_VERSION_LESS_EQUAL: + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result <= 0); + case SEMANTIC_VERSION_LESS_THAN: + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result < 0); + case SEMANTIC_VERSION_NOT_EQUAL: + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result != 0); + default: + return false; + } + } + + private boolean evaluatePercentCondition(PercentCondition condition, KeysAndValues context) { + if (!context.containsKey("randomizationId")) { + logger.warn("Percentage operation must not be performed without randomizationId"); + return false; + } + + PercentConditionOperator operator = condition.getPercentConditionOperator(); + + // The micro-percent interval to be used with the BETWEEN operator. + MicroPercentRange microPercentRange = condition.getMicroPercentRange(); + int microPercentUpperBound = + microPercentRange != null ? microPercentRange.getMicroPercentUpperBound() : 0; + int microPercentLowerBound = + microPercentRange != null ? microPercentRange.getMicroPercentLowerBound() : 0; + // The limit of percentiles to target in micro-percents when using the + // LESS_OR_EQUAL and GREATER_THAN operators. The value must be in the range [0 + // and 100000000]. + int microPercent = condition.getMicroPercent(); + BigInteger microPercentile = + getMicroPercentile(condition.getSeed(), context.get("randomizationId")); + switch (operator) { + case LESS_OR_EQUAL: + return microPercentile.compareTo(BigInteger.valueOf(microPercent)) <= 0; + case GREATER_THAN: + return microPercentile.compareTo(BigInteger.valueOf(microPercent)) > 0; + case BETWEEN: + return microPercentile.compareTo(BigInteger.valueOf(microPercentLowerBound)) > 0 + && microPercentile.compareTo(BigInteger.valueOf(microPercentUpperBound)) <= 0; + case UNSPECIFIED: + default: + return false; + } + } + + private BigInteger getMicroPercentile(String seed, String randomizationId) { + String seedPrefix = seed != null && !seed.isEmpty() ? seed + "." : ""; + String stringToHash = seedPrefix + randomizationId; + BigInteger hash = hashSeededRandomizationId(stringToHash); + BigInteger microPercentile = hash.mod(MICRO_PERCENT_MODULO); + + return microPercentile; + } + + private BigInteger hashSeededRandomizationId(String seededRandomizationId) { + try { + // Create a SHA-256 hash. + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(seededRandomizationId.getBytes(StandardCharsets.UTF_8)); + + // Convert the hash bytes to a BigInteger + return new BigInteger(1, hashBytes); + } catch (NoSuchAlgorithmException e) { + logger.error("SHA-256 algorithm not found", e); + throw new RuntimeException("SHA-256 algorithm not found", e); + } + } + + private boolean compareStrings( + ImmutableList targetValues, + String customSignal, + BiPredicate compareFunction) { + return targetValues.stream() + .anyMatch(targetValue -> compareFunction.test(customSignal, targetValue)); + } + + private boolean compareStringRegex(String customSignal, String targetSignal) { + try { + return Pattern.compile(targetSignal).matcher(customSignal).matches(); + } catch (PatternSyntaxException e) { + return false; + } + } + + private boolean compareNumbers( + ImmutableList targetValues, String customSignal, IntPredicate compareFunction) { + if (targetValues.size() != 1) { + logger.warn( + String.format( + "Target values must contain 1 element for numeric operations. Target Value: %s", + targetValues)); + return false; + } + + try { + double customSignalDouble = Double.parseDouble(customSignal); + double targetValue = Double.parseDouble(targetValues.get(0)); + int comparisonResult = Double.compare(customSignalDouble, targetValue); + return compareFunction.test(comparisonResult); + } catch (NumberFormatException e) { + logger.warn( + "Error parsing numeric values: customSignal=%s, targetValue=%s", + customSignal, targetValues.get(0), e); + return false; + } + } + + private boolean compareSemanticVersions( + ImmutableList targetValues, String customSignal, IntPredicate compareFunction) { + if (targetValues.size() != 1) { + logger.warn(String.format("Target values must contain 1 element for semantic operation.")); + return false; + } + + String targetValueString = targetValues.get(0); + if (!validateSemanticVersion(targetValueString) || !validateSemanticVersion(customSignal)) { + return false; + } + + List targetVersion = parseSemanticVersion(targetValueString); + List customSignalVersion = parseSemanticVersion(customSignal); + + int maxLength = 5; + if (targetVersion.size() > maxLength || customSignalVersion.size() > maxLength) { + logger.warn( + "Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s", + maxLength, targetValueString, customSignal); + return false; + } + + int comparison = compareSemanticVersions(customSignalVersion, targetVersion); + return compareFunction.test(comparison); + } + + private int compareSemanticVersions(List version1, List version2) { + int maxLength = Math.max(version1.size(), version2.size()); + int version1Size = version1.size(); + int version2Size = version2.size(); + + for (int i = 0; i < maxLength; i++) { + // Default to 0 if segment is missing + int v1 = i < version1Size ? version1.get(i) : 0; + int v2 = i < version2Size ? version2.get(i) : 0; + + int comparison = Integer.compare(v1, v2); + if (comparison != 0) { + return comparison; + } + } + // Versions are equal + return 0; + } + + private List parseSemanticVersion(String versionString) { + return Arrays.stream(versionString.split("\\.")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + private boolean validateSemanticVersion(String version) { + return SEMVER_PATTERN.matcher(version).matches(); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java b/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java new file mode 100644 index 000000000..a8d96efdf --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java @@ -0,0 +1,140 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.CustomSignalConditionResponse; + +import java.util.ArrayList; +import java.util.List; + +final class CustomSignalCondition { + private final String customSignalKey; + private final CustomSignalOperator customSignalOperator; + private final ImmutableList targetCustomSignalValues; + + public CustomSignalCondition( + @NonNull String customSignalKey, + @NonNull CustomSignalOperator customSignalOperator, + @NonNull List targetCustomSignalValues) { + checkArgument( + !Strings.isNullOrEmpty(customSignalKey), "Custom signal key must not be null or empty."); + checkNotNull(customSignalOperator); + checkNotNull(targetCustomSignalValues); + checkArgument( + !targetCustomSignalValues.isEmpty(), "Target custom signal values must not be empty."); + this.customSignalKey = customSignalKey.trim(); + this.customSignalOperator = customSignalOperator; + this.targetCustomSignalValues = ImmutableList.copyOf(targetCustomSignalValues); + } + + CustomSignalCondition(CustomSignalConditionResponse customSignalCondition) { + checkArgument( + !Strings.isNullOrEmpty(customSignalCondition.getKey()), + "Custom signal key must not be null or empty."); + checkArgument( + !customSignalCondition.getTargetValues().isEmpty(), + "Target custom signal values must not be empty."); + this.customSignalKey = customSignalCondition.getKey().trim(); + List targetCustomSignalValuesList = customSignalCondition.getTargetValues(); + this.targetCustomSignalValues = ImmutableList.copyOf(targetCustomSignalValuesList); + switch (customSignalCondition.getOperator()) { + case "NUMERIC_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_EQUAL; + break; + case "NUMERIC_GREATER_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_GREATER_EQUAL; + break; + case "NUMERIC_GREATER_THAN": + this.customSignalOperator = CustomSignalOperator.NUMERIC_GREATER_THAN; + break; + case "NUMERIC_LESS_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_LESS_EQUAL; + break; + case "NUMERIC_LESS_THAN": + this.customSignalOperator = CustomSignalOperator.NUMERIC_LESS_THAN; + break; + case "NUMERIC_NOT_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_NOT_EQUAL; + break; + case "SEMANTIC_VERSION_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_EQUAL; + break; + case "SEMANTIC_VERSION_GREATER_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL; + break; + case "SEMANTIC_VERSION_GREATER_THAN": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN; + break; + case "SEMANTIC_VERSION_LESS_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL; + break; + case "SEMANTIC_VERSION_LESS_THAN": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN; + break; + case "SEMANTIC_VERSION_NOT_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL; + break; + case "STRING_CONTAINS": + this.customSignalOperator = CustomSignalOperator.STRING_CONTAINS; + break; + case "STRING_CONTAINS_REGEX": + this.customSignalOperator = CustomSignalOperator.STRING_CONTAINS_REGEX; + break; + case "STRING_DOES_NOT_CONTAIN": + this.customSignalOperator = CustomSignalOperator.STRING_DOES_NOT_CONTAIN; + break; + case "STRING_EXACTLY_MATCHES": + this.customSignalOperator = CustomSignalOperator.STRING_EXACTLY_MATCHES; + break; + default: + this.customSignalOperator = CustomSignalOperator.UNSPECIFIED; + } + checkArgument( + this.customSignalOperator != CustomSignalOperator.UNSPECIFIED, + "Custom signal operator passed is invalid"); + } + + @NonNull + String getCustomSignalKey() { + return customSignalKey; + } + + @NonNull + CustomSignalOperator getCustomSignalOperator() { + return customSignalOperator; + } + + @NonNull + List getTargetCustomSignalValues() { + return new ArrayList<>(targetCustomSignalValues); + } + + CustomSignalConditionResponse toCustomConditonResponse() { + CustomSignalConditionResponse customSignalConditionResponse = + new CustomSignalConditionResponse(); + customSignalConditionResponse.setKey(this.customSignalKey); + customSignalConditionResponse.setOperator(this.customSignalOperator.getOperator()); + customSignalConditionResponse.setTargetValues(this.targetCustomSignalValues); + return customSignalConditionResponse; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java b/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java new file mode 100644 index 000000000..0c0924d62 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; + +enum CustomSignalOperator { + NUMERIC_EQUAL("NUMERIC_EQUAL"), + NUMERIC_GREATER_EQUAL("NUMERIC_GREATER_EQUAL"), + NUMERIC_GREATER_THAN("NUMERIC_GREATER_THAN"), + NUMERIC_LESS_EQUAL("NUMERIC_LESS_EQUAL"), + NUMERIC_LESS_THAN("NUMERIC_LESS_THAN"), + NUMERIC_NOT_EQUAL("NUMERIC_NOT_EQUAL"), + SEMANTIC_VERSION_EQUAL("SEMANTIC_VERSION_EQUAL"), + SEMANTIC_VERSION_GREATER_EQUAL("SEMANTIC_VERSION_GREATER_EQUAL"), + SEMANTIC_VERSION_GREATER_THAN("SEMANTIC_VERSION_GREATER_THAN"), + SEMANTIC_VERSION_LESS_EQUAL("SEMANTIC_VERSION_LESS_EQUAL"), + SEMANTIC_VERSION_LESS_THAN("SEMANTIC_VERSION_LESS_THAN"), + SEMANTIC_VERSION_NOT_EQUAL("SEMANTIC_VERSION_NOT_EQUAL"), + STRING_CONTAINS("STRING_CONTAINS"), + STRING_CONTAINS_REGEX("STRING_CONTAINS_REGEX"), + STRING_DOES_NOT_CONTAIN("STRING_DOES_NOT_CONTAIN"), + STRING_EXACTLY_MATCHES("STRING_EXACTLY_MATCHES"), + UNSPECIFIED("CUSTOM_SIGNAL_OPERATOR_UNSPECIFIED"); + + private final String operator; + + CustomSignalOperator(@NonNull String operator) { + checkArgument(!Strings.isNullOrEmpty(operator), "Operator must not be null or empty."); + this.operator = operator; + } + + @NonNull + String getOperator() { + return operator; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index 41a0afbe4..e3edce4e4 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -101,6 +101,73 @@ protected Template execute() throws FirebaseRemoteConfigException { }; } + /** + * Alternative to {@link #getServerTemplate} where developers can initialize with a pre-cached + * template or config. + */ + public ServerTemplateImpl.Builder serverTemplateBuilder() { + return new ServerTemplateImpl.Builder(this.remoteConfigClient); + } + + /** + * Initializes a template instance and loads the latest template data. + * + * @param defaultConfig Default parameter values to use if a getter references a parameter not + * found in the template. + * @return A {@link Template} instance with the latest template data. + */ + public ServerTemplate getServerTemplate(KeysAndValues defaultConfig) + throws FirebaseRemoteConfigException { + return getServerTemplateOp(defaultConfig).call(); + } + + /** + * Initializes a template instance without any defaults and loads the latest template data. + * + * @return A {@link Template} instance with the latest template data. + */ + public ServerTemplate getServerTemplate() throws FirebaseRemoteConfigException { + return getServerTemplate(null); + } + + /** + * Initializes a template instance and asynchronously loads the latest template data. + * + * @param defaultConfig Default parameter values to use if a getter references a parameter not + * found in the template. + * @return A {@link Template} instance with the latest template data. + */ + public ApiFuture getServerTemplateAsync(KeysAndValues defaultConfig) { + return getServerTemplateOp(defaultConfig).callAsync(app); + } + + /** + * Initializes a template instance without any defaults and asynchronously loads the latest + * template data. + * + * @return A {@link Template} instance with the latest template data. + */ + public ApiFuture getServerTemplateAsync() { + return getServerTemplateAsync(null); + } + + private CallableOperation getServerTemplateOp( + KeysAndValues defaultConfig) { + return new CallableOperation() { + @Override + protected ServerTemplate execute() throws FirebaseRemoteConfigException { + String serverTemplateData = remoteConfigClient.getServerTemplate(); + ServerTemplate template = + serverTemplateBuilder() + .defaultConfig(defaultConfig) + .cachedTemplate(serverTemplateData) + .build(); + + return template; + } + }; + } + /** * Gets the requested version of the of the Remote Config template. * @@ -413,3 +480,4 @@ public void destroy() { } } } + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java index 9fdb596d6..2143d07d1 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java @@ -40,4 +40,7 @@ Template publishTemplate(Template template, boolean validateOnly, ListVersionsResponse listVersions( ListVersionsOptions options) throws FirebaseRemoteConfigException; + + String getServerTemplate() throws FirebaseRemoteConfigException; } + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java index 7425673fb..d84abae84 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java @@ -38,6 +38,7 @@ import com.google.firebase.internal.NonNull; import com.google.firebase.internal.SdkUtils; import com.google.firebase.remoteconfig.internal.RemoteConfigServiceErrorResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse; import java.io.IOException; @@ -51,6 +52,9 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient private static final String REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/remoteConfig"; + private static final String SERVER_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/namespaces/firebase-server/serverRemoteConfig"; + private static final Map COMMON_HEADERS = ImmutableMap.of( "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(), @@ -62,6 +66,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient ); private final String remoteConfigUrl; + private final String serverRemoteConfigUrl; private final HttpRequestFactory requestFactory; private final JsonFactory jsonFactory; private final ErrorHandlingHttpClient httpClient; @@ -69,6 +74,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient private FirebaseRemoteConfigClientImpl(Builder builder) { checkArgument(!Strings.isNullOrEmpty(builder.projectId)); this.remoteConfigUrl = String.format(REMOTE_CONFIG_URL, builder.projectId); + this.serverRemoteConfigUrl = String.format(SERVER_REMOTE_CONFIG_URL, builder.projectId); this.requestFactory = checkNotNull(builder.requestFactory); this.jsonFactory = checkNotNull(builder.jsonFactory); HttpResponseInterceptor responseInterceptor = builder.responseInterceptor; @@ -82,6 +88,11 @@ String getRemoteConfigUrl() { return remoteConfigUrl; } + @VisibleForTesting + String getServerRemoteConfigUrl() { + return serverRemoteConfigUrl; + } + @VisibleForTesting HttpRequestFactory getRequestFactory() { return requestFactory; @@ -102,6 +113,18 @@ public Template getTemplate() throws FirebaseRemoteConfigException { return template.setETag(getETag(response)); } + @Override + public String getServerTemplate() throws FirebaseRemoteConfigException { + HttpRequestInfo request = + HttpRequestInfo.buildGetRequest(serverRemoteConfigUrl).addAllHeaders(COMMON_HEADERS); + IncomingHttpResponse response = httpClient.send(request); + ServerTemplateResponse templateResponse = httpClient.parse(response, + ServerTemplateResponse.class); + ServerTemplateData serverTemplateData = new ServerTemplateData(templateResponse); + serverTemplateData.setETag(getETag(response)); + return serverTemplateData.toJSON(); + } + @Override public Template getTemplateAtVersion( @NonNull String versionNumber) throws FirebaseRemoteConfigException { @@ -267,3 +290,4 @@ private RemoteConfigServiceErrorResponse safeParse(String response) { } } } + diff --git a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java new file mode 100644 index 000000000..47b159bf7 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java @@ -0,0 +1,135 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents data stored in context passed to server-side Remote Config. + */ +public class KeysAndValues { + final ImmutableMap keysAndValues; + + private KeysAndValues(@NonNull Builder builder) { + keysAndValues = ImmutableMap.builder().putAll(builder.keysAndValues).build(); + } + + /** + * Checks whether a key is present in the context. + * + * @param key The key for data stored in context. + * @return Boolean representing whether the key passed is present in context. + */ + public boolean containsKey(String key) { + return keysAndValues.containsKey(key); + } + + /** + * Gets the value of the data stored in context. + * + * @param key The key for data stored in context. + * @return Value assigned to the key in context. + */ + public String get(String key) { + return keysAndValues.get(key); + } + + /** + * Builder class for KeysAndValues using which values will be assigned to + * private variables. + */ + public static class Builder { + // Holds the converted pairs of custom keys and values. + private final Map keysAndValues = new HashMap<>(); + + /** + * Adds a context data with string value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, @NonNull String value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + checkArgument(!Strings.isNullOrEmpty(value), "Context key must not be null or empty."); + keysAndValues.put(key, value); + return this; + } + + /** + * Adds a context data with boolean value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, boolean value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + keysAndValues.put(key, Boolean.toString(value)); + return this; + } + + /** + * Adds a context data with double value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, double value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + keysAndValues.put(key, Double.toString(value)); + return this; + } + + /** + * Adds a context data with long value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, long value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + keysAndValues.put(key, Long.toString(value)); + return this; + } + + /** + * Creates an instance of KeysAndValues with the values assigned through + * builder. + * + * @return instance of KeysAndValues + */ + @NonNull + public KeysAndValues build() { + return new KeysAndValues(this); + } + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java b/src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java new file mode 100644 index 000000000..abd5711c6 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java @@ -0,0 +1,46 @@ +/* +* Copyright 2025 Google LLC +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT 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.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.MicroPercentRangeResponse; + +class MicroPercentRange { + private final int microPercentLowerBound; + private final int microPercentUpperBound; + + public MicroPercentRange(@Nullable Integer microPercentLowerBound, + @Nullable Integer microPercentUpperBound) { + this.microPercentLowerBound = microPercentLowerBound != null ? microPercentLowerBound : 0; + this.microPercentUpperBound = microPercentUpperBound != null ? microPercentUpperBound : 0; + } + + int getMicroPercentLowerBound() { + return microPercentLowerBound; + } + + int getMicroPercentUpperBound() { + return microPercentUpperBound; + } + + MicroPercentRangeResponse toMicroPercentRangeResponse() { + MicroPercentRangeResponse microPercentRangeResponse = new MicroPercentRangeResponse(); + microPercentRangeResponse.setMicroPercentLowerBound(this.microPercentLowerBound); + microPercentRangeResponse.setMicroPercentUpperBound(this.microPercentUpperBound); + return microPercentRangeResponse; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java new file mode 100644 index 000000000..be3f5fd3e --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java @@ -0,0 +1,140 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; + +class OneOfCondition { + private OrCondition orCondition; + private AndCondition andCondition; + private PercentCondition percent; + private CustomSignalCondition customSignal; + private String trueValue; + private String falseValue; + + OneOfCondition(OneOfConditionResponse oneOfconditionResponse) { + if (oneOfconditionResponse.getOrCondition() != null) { + this.orCondition = new OrCondition(oneOfconditionResponse.getOrCondition()); + } + if (oneOfconditionResponse.getAndCondition() != null) { + this.andCondition = new AndCondition(oneOfconditionResponse.getAndCondition()); + } + if (oneOfconditionResponse.getPercentCondition() != null) { + this.percent = new PercentCondition(oneOfconditionResponse.getPercentCondition()); + } + if (oneOfconditionResponse.getCustomSignalCondition() != null) { + this.customSignal = + new CustomSignalCondition(oneOfconditionResponse.getCustomSignalCondition()); + } + } + + @VisibleForTesting + OneOfCondition() { + this.orCondition = null; + this.andCondition = null; + this.percent = null; + this.trueValue = null; + this.falseValue = null; + } + + @Nullable + OrCondition getOrCondition() { + return orCondition; + } + + @Nullable + AndCondition getAndCondition() { + return andCondition; + } + + @Nullable + String isTrue() { + return trueValue; + } + + @Nullable + String isFalse() { + return falseValue; + } + + @Nullable + PercentCondition getPercent() { + return percent; + } + + @Nullable + CustomSignalCondition getCustomSignal() { + return customSignal; + } + + OneOfCondition setOrCondition(@NonNull OrCondition orCondition) { + checkNotNull(orCondition, "`Or` condition cannot be set to null."); + this.orCondition = orCondition; + return this; + } + + OneOfCondition setAndCondition(@NonNull AndCondition andCondition) { + checkNotNull(andCondition, "`And` condition cannot be set to null."); + this.andCondition = andCondition; + return this; + } + + OneOfCondition setPercent(@NonNull PercentCondition percent) { + checkNotNull(percent, "`Percent` condition cannot be set to null."); + this.percent = percent; + return this; + } + + OneOfCondition setCustomSignal(@NonNull CustomSignalCondition customSignal) { + checkNotNull(customSignal, "`Custom signal` condition cannot be set to null."); + this.customSignal = customSignal; + return this; + } + + OneOfCondition setTrue() { + this.trueValue = "true"; + return this; + } + + OneOfCondition setFalse() { + this.falseValue = "false"; + return this; + } + + OneOfConditionResponse toOneOfConditionResponse() { + OneOfConditionResponse oneOfConditionResponse = new OneOfConditionResponse(); + if (this.andCondition != null) { + oneOfConditionResponse.setAndCondition(this.andCondition.toAndConditionResponse()); + } + if (this.orCondition != null) { + oneOfConditionResponse.setOrCondition(this.orCondition.toOrConditionResponse()); + } + if (this.customSignal != null) { + oneOfConditionResponse.setCustomSignalCondition(this.customSignal.toCustomConditonResponse()); + } + if (this.percent != null) { + oneOfConditionResponse.setPercentCondition(this.percent.toPercentConditionResponse()); + } + return oneOfConditionResponse; + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java new file mode 100644 index 000000000..36a1c682a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OrConditionResponse; +import java.util.List; +import java.util.stream.Collectors; + +final class OrCondition { + private final ImmutableList conditions; + + public OrCondition(@NonNull List conditions) { + checkNotNull(conditions, "List of conditions for OR operation must not be null."); + checkArgument(!conditions.isEmpty(), "List of conditions for OR operation must not be empty."); + this.conditions = ImmutableList.copyOf(conditions); + } + + OrCondition(OrConditionResponse orConditionResponse) { + List conditionList = orConditionResponse.getConditions(); + checkNotNull(conditionList, "List of conditions for AND operation cannot be null."); + checkArgument(!conditionList.isEmpty(), "List of conditions for AND operation cannot be empty"); + this.conditions = conditionList.stream() + .map(OneOfCondition::new) + .collect(ImmutableList.toImmutableList()); + } + + @NonNull + ImmutableList getConditions() { + return conditions; + } + + OrConditionResponse toOrConditionResponse() { + return new OrConditionResponse() + .setConditions(this.conditions.stream() + .map(OneOfCondition::toOneOfConditionResponse) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/PercentCondition.java b/src/main/java/com/google/firebase/remoteconfig/PercentCondition.java new file mode 100644 index 000000000..c5763200b --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/PercentCondition.java @@ -0,0 +1,163 @@ +/* +* Copyright 2025 Google LLC +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.PercentConditionResponse; + +/** Represents a condition that compares the instance pseudo-random percentile to a given limit. */ +public final class PercentCondition { + private int microPercent; + private MicroPercentRange microPercentRange; + private final PercentConditionOperator percentConditionOperator; + private final String seed; + + /** + * Create a percent condition for operator BETWEEN. + * + * @param microPercent The limit of percentiles to target in micro-percents when using the + * LESS_OR_EQUAL and GREATER_THAN operators. The value must be in the range [0 and 100000000]. + * @param percentConditionOperator The choice of percent operator to determine how to compare + * targets to percent(s). + * @param seed The seed used when evaluating the hash function to map an instance to a value in + * the hash space. This is a string which can have 0 - 32 characters and can contain ASCII + * characters [-_.0-9a-zA-Z].The string is case-sensitive. + */ + PercentCondition( + @Nullable Integer microPercent, + @NonNull PercentConditionOperator percentConditionOperator, + @NonNull String seed) { + checkNotNull(percentConditionOperator, "Percentage operator must not be null."); + checkArgument(!Strings.isNullOrEmpty(seed), "Seed must not be null or empty."); + this.microPercent = microPercent != null ? microPercent : 0; + this.percentConditionOperator = percentConditionOperator; + this.seed = seed; + } + + /** + * Create a percent condition for operators GREATER_THAN and LESS_OR_EQUAL. + * + * @param microPercentRange The micro-percent interval to be used with the BETWEEN operator. + * @param percentConditionOperator The choice of percent operator to determine how to compare + * targets to percent(s). + * @param seed The seed used when evaluating the hash function to map an instance to a value in + * the hash space. This is a string which can have 0 - 32 characters and can contain ASCII + * characters [-_.0-9a-zA-Z].The string is case-sensitive. + */ + PercentCondition( + @NonNull MicroPercentRange microPercentRange, + @NonNull PercentConditionOperator percentConditionOperator, + String seed) { + checkNotNull(microPercentRange, "Percent range must not be null."); + checkNotNull(percentConditionOperator, "Percentage operator must not be null."); + this.microPercentRange = microPercentRange; + this.percentConditionOperator = percentConditionOperator; + this.seed = seed; + } + + /** + * Creates a new {@link PercentCondition} from API response. + * + * @param percentCondition the conditions obtained from server call. + */ + PercentCondition(PercentConditionResponse percentCondition) { + checkArgument( + !Strings.isNullOrEmpty(percentCondition.getSeed()), "Seed must not be empty or null"); + this.microPercent = percentCondition.getMicroPercent(); + this.seed = percentCondition.getSeed(); + switch (percentCondition.getPercentOperator()) { + case "BETWEEN": + this.percentConditionOperator = PercentConditionOperator.BETWEEN; + break; + case "GREATER_THAN": + this.percentConditionOperator = PercentConditionOperator.GREATER_THAN; + break; + case "LESS_OR_EQUAL": + this.percentConditionOperator = PercentConditionOperator.LESS_OR_EQUAL; + break; + default: + this.percentConditionOperator = PercentConditionOperator.UNSPECIFIED; + } + checkArgument( + this.percentConditionOperator != PercentConditionOperator.UNSPECIFIED, + "Percentage operator is invalid"); + if (percentCondition.getMicroPercentRange() != null) { + this.microPercentRange = + new MicroPercentRange( + percentCondition.getMicroPercentRange().getMicroPercentLowerBound(), + percentCondition.getMicroPercentRange().getMicroPercentUpperBound()); + } + } + + /** + * Gets the limit of percentiles to target in micro-percents when using the LESS_OR_EQUAL and + * GREATER_THAN operators. The value must be in the range [0 and 100000000]. + * + * @return micro percent. + */ + @Nullable + public int getMicroPercent() { + return microPercent; + } + + /** + * Gets micro-percent interval to be used with the BETWEEN operator. + * + * @return micro percent range. + */ + @Nullable + public MicroPercentRange getMicroPercentRange() { + return microPercentRange; + } + + /** + * Gets choice of percent operator to determine how to compare targets to percent(s). + * + * @return operator. + */ + @NonNull + public PercentConditionOperator getPercentConditionOperator() { + return percentConditionOperator; + } + + /** + * The seed used when evaluating the hash function to map an instance to a value in the hash + * space. This is a string which can have 0 - 32 characters and can contain ASCII characters + * [-_.0-9a-zA-Z].The string is case-sensitive. + * + * @return seed. + */ + @NonNull + public String getSeed() { + return seed; + } + + PercentConditionResponse toPercentConditionResponse() { + PercentConditionResponse percentConditionResponse = new PercentConditionResponse(); + percentConditionResponse.setMicroPercent(this.microPercent); + percentConditionResponse.setMicroPercentRange( + this.microPercentRange.toMicroPercentRangeResponse()); + percentConditionResponse.setPercentOperator(this.percentConditionOperator.getOperator()); + percentConditionResponse.setSeed(this.seed); + return percentConditionResponse; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java b/src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java new file mode 100644 index 000000000..478f13e4e --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java @@ -0,0 +1,55 @@ +/* +* Copyright 2025 Google LLC +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT 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 com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; + +/** +* Defines supported operators for percent conditions. +*/ +public enum PercentConditionOperator { + BETWEEN("BETWEEN"), + GREATER_THAN("GREATER_THAN"), + LESS_OR_EQUAL("LESS_OR_EQUAL"), + UNSPECIFIED("PERCENT_OPERATOR_UNSPECIFIED"); + + private final String operator; + + /** + * Creates percent condition operator. + * + * @param operator The choice of percent operator to determine how to compare targets to + * percent(s). + */ + PercentConditionOperator(@NonNull String operator) { + checkArgument(!Strings.isNullOrEmpty(operator), "Operator must not be null or empty."); + this.operator = operator; + } + + /** + * Gets percent condition operator. + * + * @return operator. + */ + @NonNull + public String getOperator() { + return operator; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java b/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java new file mode 100644 index 000000000..f16aeffc8 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.ServerConditionResponse; + +import java.util.Objects; + +final class ServerCondition { + + private String name; + private OneOfCondition serverCondition; + + ServerCondition(@NonNull String name, @NonNull OneOfCondition condition) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + this.serverCondition = condition; + } + + ServerCondition(@NonNull ServerConditionResponse serverConditionResponse) { + checkNotNull(serverConditionResponse); + this.name = serverConditionResponse.getName(); + this.serverCondition = new OneOfCondition(serverConditionResponse.getServerCondition()); + } + + @NonNull + String getName() { + return name; + } + + @NonNull + OneOfCondition getCondition() { + return serverCondition; + } + + ServerCondition setName(@NonNull String name) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + return this; + } + + ServerCondition setServerCondition(@NonNull OneOfCondition condition) { + checkNotNull(condition, "condition must not be null or empty"); + this.serverCondition = condition; + return this; + } + + ServerConditionResponse toServerConditionResponse() { + return new ServerConditionResponse().setName(this.name) + .setServerCondition(this.serverCondition.toOneOfConditionResponse()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerCondition condition = (ServerCondition) o; + return Objects.equals(name, condition.name) + && Objects.equals(serverCondition, condition.serverCondition); + } + + @Override + public int hashCode() { + return Objects.hash(name, serverCondition); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java new file mode 100644 index 000000000..8540578b0 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; + +import java.util.Map; + +/** + * Represents the configuration produced by evaluating a server template. + */ +public final class ServerConfig { + private final Map configValues; + + ServerConfig(Map configValues) { + this.configValues = configValues; + } + + /** + * Gets the value for the given key as a string. Convenience method for calling + * serverConfig.getValue(key).asString(). + * + * @param key The name of the parameter. + * @return config value for the given key as string. + */ + @NonNull + public String getString(@NonNull String key) { + return this.getValue(key).asString(); + } + + /** + * Gets the value for the given key as a boolean.Convenience method for calling + * serverConfig.getValue(key).asBoolean(). + * + * @param key The name of the parameter. + * @return config value for the given key as boolean. + */ + @NonNull + public boolean getBoolean(@NonNull String key) { + return this.getValue(key).asBoolean(); + } + + /** + * Gets the value for the given key as long.Convenience method for calling + * serverConfig.getValue(key).asLong(). + * + * @param key The name of the parameter. + * @return config value for the given key as long. + */ + @NonNull + public long getLong(@NonNull String key) { + return this.getValue(key).asLong(); + } + + /** + * Gets the value for the given key as double.Convenience method for calling + * serverConfig.getValue(key).asDouble(). + * + * @param key The name of the parameter. + * @return config value for the given key as double. + */ + @NonNull + public double getDouble(@NonNull String key) { + return this.getValue(key).asDouble(); + } + + /** + * Gets the {@link ValueSource} for the given key. + * + * @param key The name of the parameter. + * @return config value source for the given key. + */ + @NonNull + public ValueSource getValueSource(@NonNull String key) { + return this.getValue(key).getSource(); + } + + private Value getValue(String key) { + checkArgument(!Strings.isNullOrEmpty(key), "Server config key cannot be null or empty."); + if (configValues.containsKey(key)) { + return configValues.get(key); + } + return new Value(ValueSource.STATIC); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java new file mode 100644 index 000000000..bd1a59940 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.api.core.ApiFuture; + +public interface ServerTemplate { + public interface Builder { + + Builder defaultConfig(KeysAndValues config); + + Builder cachedTemplate(String templateJson); + + ServerTemplate build(); + } + /** + * Process the template data with a condition evaluator + * based on the provided context. + */ + ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigException; + /** + * Process the template data without context. + */ + ServerConfig evaluate() throws FirebaseRemoteConfigException; + /** + * Fetches and caches the current active version of the project. + */ + ApiFuture load() throws FirebaseRemoteConfigException; + + String toJson(); +} diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java new file mode 100644 index 000000000..59d51b51a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -0,0 +1,215 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ServerTemplateData { + + private String etag; + private Map parameters; + private List serverConditions; + private Map parameterGroups; + private Version version; + + + ServerTemplateData(String etag) { + this.parameters = new HashMap<>(); + this.serverConditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + this.etag = etag; + } + + ServerTemplateData() { + this((String) null); + } + + ServerTemplateData(@NonNull ServerTemplateResponse serverTemplateResponse) { + checkNotNull(serverTemplateResponse); + this.parameters = new HashMap<>(); + this.serverConditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + if (serverTemplateResponse.getParameters() != null) { + for (Map.Entry entry : + serverTemplateResponse.getParameters().entrySet()) { + this.parameters.put(entry.getKey(), new Parameter(entry.getValue())); + } + } + if (serverTemplateResponse.getServerConditions() != null) { + for (ServerTemplateResponse.ServerConditionResponse conditionResponse : + serverTemplateResponse.getServerConditions()) { + this.serverConditions.add(new ServerCondition(conditionResponse)); + } + } + if (serverTemplateResponse.getParameterGroups() != null) { + for (Map.Entry entry : + serverTemplateResponse.getParameterGroups().entrySet()) { + this.parameterGroups.put(entry.getKey(), new ParameterGroup(entry.getValue())); + } + } + if (serverTemplateResponse.getVersion() != null) { + this.version = new Version(serverTemplateResponse.getVersion()); + } + this.etag = serverTemplateResponse.getEtag(); + } + + + static ServerTemplateData fromJSON(@NonNull String json) + throws FirebaseRemoteConfigException { + checkArgument(!Strings.isNullOrEmpty(json), "JSON String must not be null or empty."); + // using the default json factory as no rpc calls are made here + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + try { + ServerTemplateResponse serverTemplateResponse = + jsonFactory.createJsonParser(json).parseAndClose(ServerTemplateResponse.class); + return new ServerTemplateData(serverTemplateResponse); + } catch (IOException e) { + throw new FirebaseRemoteConfigException( + ErrorCode.INVALID_ARGUMENT, "Unable to parse JSON string."); + } + } + + + String getETag() { + return this.etag; + } + + + @NonNull + public Map getParameters() { + return this.parameters; + } + + @NonNull + List getServerConditions() { + return serverConditions; + } + + @NonNull + Map getParameterGroups() { + return parameterGroups; + } + + Version getVersion() { + return version; + } + + ServerTemplateData setParameters(@NonNull Map parameters) { + checkNotNull(parameters, "parameters must not be null."); + this.parameters = parameters; + return this; + } + + + ServerTemplateData setServerConditions(@NonNull List conditions) { + checkNotNull(conditions, "conditions must not be null."); + this.serverConditions = conditions; + return this; + } + + ServerTemplateData setParameterGroups( + @NonNull Map parameterGroups) { + checkNotNull(parameterGroups, "parameter groups must not be null."); + this.parameterGroups = parameterGroups; + return this; + } + + ServerTemplateData setVersion(Version version) { + this.version = version; + return this; + } + + String toJSON() { + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + try { + return jsonFactory.toString(this.toServerTemplateResponse(true)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + ServerTemplateData setETag(String etag) { + this.etag = etag; + return this; + } + + ServerTemplateResponse toServerTemplateResponse(boolean includeAll) { + Map parameterResponses = new HashMap<>(); + for (Map.Entry entry : this.parameters.entrySet()) { + parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse()); + } + List serverConditionResponses = + new ArrayList<>(); + for (ServerCondition condition : this.serverConditions) { + serverConditionResponses.add(condition.toServerConditionResponse()); + } + Map parameterGroupResponse = new HashMap<>(); + for (Map.Entry entry : this.parameterGroups.entrySet()) { + parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse()); + } + TemplateResponse.VersionResponse versionResponse = + (this.version == null) ? null : this.version.toVersionResponse(includeAll); + ServerTemplateResponse serverTemplateResponse = + new ServerTemplateResponse() + .setParameters(parameterResponses) + .setServerConditions(serverConditionResponses) + .setParameterGroups(parameterGroupResponse) + .setVersion(versionResponse); + if (includeAll) { + return serverTemplateResponse.setEtag(this.etag); + } + return serverTemplateResponse; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerTemplateData template = (ServerTemplateData) o; + return Objects.equals(etag, template.etag) + && Objects.equals(parameters, template.parameters) + && Objects.equals(serverConditions, template.serverConditions) + && Objects.equals(parameterGroups, template.parameterGroups) + && Objects.equals(version, template.version); + } + + @Override + public int hashCode() { + return Objects.hash(etag, parameters, serverConditions, parameterGroups, version); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java new file mode 100644 index 000000000..742c19803 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -0,0 +1,195 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ServerTemplateImpl implements ServerTemplate { + + private final KeysAndValues defaultConfig; + private FirebaseRemoteConfigClient client; + private AtomicReference cache; + private final AtomicReference cachedTemplate; + private static final Logger logger = LoggerFactory.getLogger(ServerTemplate.class); + + public static class Builder implements ServerTemplate.Builder { + private KeysAndValues defaultConfig; + private String cachedTemplate; + private FirebaseRemoteConfigClient client; + + Builder(FirebaseRemoteConfigClient remoteConfigClient) { + this.client = remoteConfigClient; + } + + @Override + public Builder defaultConfig(KeysAndValues config) { + this.defaultConfig = config; + return this; + } + + @Override + public Builder cachedTemplate(String templateJson) { + this.cachedTemplate = templateJson; + return this; + } + + @Override + public ServerTemplate build() { + return new ServerTemplateImpl(this); + } + } + + private ServerTemplateImpl(Builder builder) { + this.defaultConfig = builder.defaultConfig; + this.cachedTemplate = new AtomicReference<>(builder.cachedTemplate); + this.client = builder.client; + this.cache = new AtomicReference<>(null); + + String initialTemplate = this.cachedTemplate.get(); + try { + this.cache.set(ServerTemplateData.fromJSON(initialTemplate)); + } catch (FirebaseRemoteConfigException e) { + e.printStackTrace(); + } + } + + @Override + public ServerConfig evaluate(@Nullable KeysAndValues context) + throws FirebaseRemoteConfigException { + ServerTemplateData cachedData = this.cache.get(); + if (cachedData == null) { + throw new FirebaseRemoteConfigException(ErrorCode.FAILED_PRECONDITION, + "No Remote Config Server template in cache. Call load() before calling evaluate()."); + } + + Map configValues = new HashMap<>(); + ImmutableMap defaultConfigValues = defaultConfig.keysAndValues; + // Initializes configValue objects with default values. + for (String configName : defaultConfigValues.keySet()) { + configValues.put(configName, new Value(ValueSource.DEFAULT, + defaultConfigValues.get(configName))); + } + + ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); + ImmutableMap evaluatedCondition = ImmutableMap.copyOf( + conditionEvaluator.evaluateConditions(cachedData.getServerConditions(), context)); + ImmutableMap parameters = ImmutableMap.copyOf(cachedData.getParameters()); + mergeDerivedConfigValues(evaluatedCondition, parameters, configValues); + + return new ServerConfig(configValues); + } + + @Override + public ServerConfig evaluate() throws FirebaseRemoteConfigException { + return evaluate(null); + } + + @Override + public ApiFuture load() throws FirebaseRemoteConfigException { + String serverTemplate = client.getServerTemplate(); + this.cachedTemplate.set(serverTemplate); + this.cache.set(ServerTemplateData.fromJSON(serverTemplate)); + return ApiFutures.immediateFuture(null); + } + + // Add getters or other methods as needed + public KeysAndValues getDefaultConfig() { + return defaultConfig; + } + + public String getCachedTemplate() { + return cachedTemplate.get(); + } + + @Override + public String toJson() { + ServerTemplateData currentCache = this.cache.get(); + if (currentCache == null) { + return "{}"; + } + return currentCache.toJSON(); + } + + private void mergeDerivedConfigValues(ImmutableMap evaluatedCondition, + ImmutableMap parameters, Map configValues) { + for (String parameterName : parameters.keySet()) { + Parameter parameter = parameters.get(parameterName); + if (parameter == null) { + logger.warn(String.format("Parameter value is not assigned for %s", parameterName)); + continue; + } + + ImmutableMap conditionalValues = ImmutableMap.copyOf( + parameter.getConditionalValues()); + ParameterValue derivedValue = null; + + // Iterates in order over condition list. If there is a value associated + // with a condition, this checks if the condition is true. + for (String conditionName : evaluatedCondition.keySet()) { + boolean conditionEvaluation = evaluatedCondition.get(conditionName); + if (conditionalValues.containsKey(conditionName) && conditionEvaluation) { + derivedValue = conditionalValues.get(conditionName); + break; + } + } + + if (derivedValue != null && derivedValue.toParameterValueResponse().isUseInAppDefault()) { + logger.warn( + String.format("Derived value found for %s but parameter is set to use in app default.", + parameterName)); + continue; + } + + if (derivedValue != null) { + String parameterValue = derivedValue.toParameterValueResponse().getValue(); + Value value = new Value(ValueSource.REMOTE, parameterValue); + configValues.put(parameterName, value); + continue; + } + + ParameterValue defaultValue = parameter.getDefaultValue(); + if (defaultValue == null) { + logger.warn(String.format("Default parameter value for %s is not set.", + parameterName)); + continue; + } + + ParameterValueResponse defaultValueResponse = defaultValue.toParameterValueResponse(); + if (defaultValueResponse != null && defaultValueResponse.isUseInAppDefault()) { + logger.info(String.format("Default value for %s is set to use in app default.", + parameterName)); + continue; + } + + String parameterDefaultValue = defaultValue.toParameterValueResponse().getValue(); + Value value = new Value(ValueSource.REMOTE, parameterDefaultValue); + configValues.put(parameterName, value); + } + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/Value.java b/src/main/java/com/google/firebase/remoteconfig/Value.java new file mode 100644 index 000000000..7935e8491 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/Value.java @@ -0,0 +1,135 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps a parameter value with metadata and type-safe getters. Type-safe + * getters insulate application logic from remote changes to parameter names and + * types. + */ +class Value { + private static final Logger logger = LoggerFactory.getLogger(Value.class); + private static final boolean DEFAULT_VALUE_FOR_BOOLEAN = false; + private static final String DEFAULT_VALUE_FOR_STRING = ""; + private static final long DEFAULT_VALUE_FOR_LONG = 0; + private static final double DEFAULT_VALUE_FOR_DOUBLE = 0; + private static final ImmutableList BOOLEAN_TRUTHY_VALUES = ImmutableList.of("1", "true", + "t", "yes", "y", "on"); + + private final ValueSource source; + private final String value; + + /** + * Creates a new {@link Value} object. + * + * @param source Indicates the source of a value. + * @param value Indicates a parameter value. + */ + Value(@NonNull ValueSource source, String value) { + checkNotNull(source, "Value source cannot be null."); + this.source = source; + this.value = value; + } + + /** + * Creates a new {@link Value} object with default value. + * + * @param source Indicates the source of a value. + */ + Value(@NonNull ValueSource source) { + this(source, DEFAULT_VALUE_FOR_STRING); + } + + /** + * Gets the value as a string. + * + * @return value as string + */ + @NonNull + String asString() { + return this.value; + } + + /** + * Gets the value as a boolean.The following values (case + * insensitive) are interpreted as true: "1", "true", "t", "yes", "y", "on". + * Other values are interpreted as false. + * + * @return value as boolean + */ + @NonNull + boolean asBoolean() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_BOOLEAN; + } + return BOOLEAN_TRUTHY_VALUES.contains(value.toLowerCase()); + } + + /** + * Gets the value as long. Comparable to calling Number(value) || 0. + * + * @return value as long + */ + @NonNull + long asLong() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_LONG; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + logger.warn("Unable to convert {} to long type.", value); + return DEFAULT_VALUE_FOR_LONG; + } + } + + /** + * Gets the value as double. Comparable to calling Number(value) || 0. + * + * @return value as double + */ + @NonNull + double asDouble() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_DOUBLE; + } + try { + return Double.parseDouble(this.value); + } catch (NumberFormatException e) { + logger.warn("Unable to convert {} to double type.", value); + return DEFAULT_VALUE_FOR_DOUBLE; + } + } + + /** + * Gets the {@link ValueSource} for the given key. + * + * @return source. + */ + @NonNull + ValueSource getSource() { + return source; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/ValueSource.java b/src/main/java/com/google/firebase/remoteconfig/ValueSource.java new file mode 100644 index 000000000..c870e8514 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ValueSource.java @@ -0,0 +1,31 @@ + +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +/** + * Indicates the source of a value. + * "static" indicates the value was defined by a static constant. + * "default" indicates the value was defined by default config. + * "remote" indicates the value was defined by config produced by evaluating a template. + */ +public enum ValueSource { + STATIC, + REMOTE, + DEFAULT +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java new file mode 100644 index 000000000..db3785afe --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -0,0 +1,322 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.internal; + +import com.google.api.client.util.Key; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterGroupResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; + +import java.util.List; +import java.util.Map; + +/** + * The Data Transfer Object for parsing Remote Config template responses from + * the Remote Config + * service. + */ +public final class ServerTemplateResponse { + @Key("parameters") + private Map parameters; + + @Key("conditions") + private List serverConditions; + + @Key("parameterGroups") + private Map parameterGroups; + + @Key("version") + private VersionResponse version; + + // For local JSON serialization and deserialization purposes only. + // ETag in response type is never set by the HTTP response. + @Key("etag") + private String etag; + + public Map getParameters() { + return parameters; + } + + public List getServerConditions() { + return serverConditions; + } + + public Map getParameterGroups() { + return parameterGroups; + } + + public VersionResponse getVersion() { + return version; + } + + public String getEtag() { + return etag; + } + + public ServerTemplateResponse setParameters(Map parameters) { + this.parameters = parameters; + return this; + } + + public ServerTemplateResponse setServerConditions( + List serverConditions) { + this.serverConditions = serverConditions; + return this; + } + + public ServerTemplateResponse setParameterGroups( + Map parameterGroups) { + this.parameterGroups = parameterGroups; + return this; + } + + public ServerTemplateResponse setVersion(VersionResponse version) { + this.version = version; + return this; + } + + public ServerTemplateResponse setEtag(String etag) { + this.etag = etag; + return this; + } + + /** + * The Data Transfer Object for parsing Remote Config condition responses from + * the Remote Config + * service. + */ + public static final class ServerConditionResponse { + + @Key("name") + private String name; + + @Key("condition") + private OneOfConditionResponse condition; + + public String getName() { + return name; + } + + public OneOfConditionResponse getServerCondition() { + return condition; + } + + public ServerConditionResponse setName(String name) { + this.name = name; + return this; + } + + public ServerConditionResponse setServerCondition(OneOfConditionResponse condition) { + this.condition = condition; + return this; + } + } + + public static final class OneOfConditionResponse { + @Key("orCondition") + private OrConditionResponse orCondition; + + @Key("andCondition") + private AndConditionResponse andCondition; + + @Key("customSignal") + private CustomSignalConditionResponse customSignalCondition; + + @Key("percent") + private PercentConditionResponse percentCondition; + + public OrConditionResponse getOrCondition() { + return orCondition; + } + + public AndConditionResponse getAndCondition() { + return andCondition; + } + + public PercentConditionResponse getPercentCondition() { + return percentCondition; + } + + public CustomSignalConditionResponse getCustomSignalCondition() { + return customSignalCondition; + } + + public OneOfConditionResponse setOrCondition(OrConditionResponse orCondition) { + this.orCondition = orCondition; + return this; + } + + public OneOfConditionResponse setAndCondition(AndConditionResponse andCondition) { + this.andCondition = andCondition; + return this; + } + + public OneOfConditionResponse setCustomSignalCondition( + CustomSignalConditionResponse customSignalCondition) { + this.customSignalCondition = customSignalCondition; + return this; + } + + public OneOfConditionResponse setPercentCondition(PercentConditionResponse percentCondition) { + this.percentCondition = percentCondition; + return this; + } + } + + public static final class OrConditionResponse { + @Key("conditions") + private List conditions; + + public List getConditions() { + return conditions; + } + + public OrConditionResponse setConditions(List conditions) { + this.conditions = conditions; + return this; + } + } + + public static final class AndConditionResponse { + @Key("conditions") + private List conditions; + + public List getConditions() { + return conditions; + } + + public AndConditionResponse setConditions(List conditions) { + this.conditions = conditions; + return this; + } + } + + public static final class CustomSignalConditionResponse { + @Key("customSignalOperator") + private String operator; + + @Key("customSignalKey") + private String key; + + @Key("targetCustomSignalValues") + private List targetValues; + + public String getOperator() { + return operator; + } + + public String getKey() { + return key; + } + + public List getTargetValues() { + return targetValues; + } + + public CustomSignalConditionResponse setOperator(String operator) { + this.operator = operator; + return this; + } + + public CustomSignalConditionResponse setKey(String key) { + this.key = key; + return this; + } + + public CustomSignalConditionResponse setTargetValues(List targetValues) { + this.targetValues = targetValues; + return this; + } + } + + public static final class PercentConditionResponse { + @Key("microPercent") + private int microPercent; + + @Key("microPercentRange") + private MicroPercentRangeResponse microPercentRange; + + @Key("percentOperator") + private String percentOperator; + + @Key("seed") + private String seed; + + public int getMicroPercent() { + return microPercent; + } + + public MicroPercentRangeResponse getMicroPercentRange() { + return microPercentRange; + } + + public String getPercentOperator() { + return percentOperator; + } + + public String getSeed() { + return seed; + } + + public PercentConditionResponse setMicroPercent(int microPercent) { + this.microPercent = microPercent; + return this; + } + + public PercentConditionResponse setMicroPercentRange( + MicroPercentRangeResponse microPercentRange) { + this.microPercentRange = microPercentRange; + return this; + } + + public PercentConditionResponse setPercentOperator(String percentOperator) { + this.percentOperator = percentOperator; + return this; + } + + public PercentConditionResponse setSeed(String seed) { + this.seed = seed; + return this; + } + } + + public static final class MicroPercentRangeResponse { + @Key("microPercentLowerBound") + private int microPercentLowerBound; + + @Key("microPercentUpperBound") + private int microPercentUpperBound; + + public int getMicroPercentLowerBound() { + return microPercentLowerBound; + } + + public int getMicroPercentUpperBound() { + return microPercentUpperBound; + } + + public MicroPercentRangeResponse setMicroPercentLowerBound(int microPercentLowerBound) { + this.microPercentLowerBound = microPercentLowerBound; + return this; + } + + public MicroPercentRangeResponse setMicroPercentUpperBound(int microPercentUpperBound) { + this.microPercentUpperBound = microPercentUpperBound; + return this; + } + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java new file mode 100644 index 000000000..0cd6e4528 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java @@ -0,0 +1,832 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; + +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +import org.junit.Test; + +public class ConditionEvaluatorTest { + + private final ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); + + @Test + public void testEvaluateConditionsEmptyOrConditionThrowsException() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> createOneOfOrCondition(null)); + assertEquals("List of conditions for OR operation must not be empty.", error.getMessage()); + } + + @Test + public void testEvaluateConditionsEmptyOrAndConditionThrowsException() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> createOneOfAndCondition(null)); + assertEquals("List of conditions for AND operation must not be empty.", error.getMessage()); + } + + @Test + public void testEvaluateConditionsOrAndTrueToTrue() { + OneOfCondition oneOfConditionTrue = createOneOfTrueCondition(); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionTrue); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues context = new KeysAndValues.Builder().build(); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + context); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsOrAndFalseToFalse() { + OneOfCondition oneOfConditionFalse = createOneOfFalseCondition(); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionFalse); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues context = new KeysAndValues.Builder().build(); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + context); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsNonOrTopConditionToTrue() { + OneOfCondition oneOfConditionTrue = createOneOfTrueCondition(); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionTrue); + KeysAndValues context = new KeysAndValues.Builder().build(); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + context); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionWithInvalidOperatorToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(0, + PercentConditionOperator.UNSPECIFIED, "seed"); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionPercent); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "abc"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionLessOrEqualMaxToTrue() { + OneOfCondition oneOfConditionPercent = createPercentCondition(10_000_0000, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionLessOrEqualMinToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(0, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionUndefinedMicroPercentToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(null, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsUseZeroForUndefinedPercentRange() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(null, null, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsUseZeroForUndefinedUpperBound() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(0, null, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsUseZeroForUndefinedLowerBound() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(null, 10_000_0000, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsGreaterThanMinToTrue() { + OneOfCondition oneOfConditionPercent = createPercentCondition(0, + PercentConditionOperator.GREATER_THAN, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsGreaterThanMaxToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(10_000_0000, + PercentConditionOperator.GREATER_THAN, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsBetweenMinAndMaxToTrue() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(0, 10_000_0000, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsBetweenEqualBoundsToFalse() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(5_000_000, + 5_000_000, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsLessOrEqualToApprox() { + OneOfCondition oneOfConditionPerCondition = createPercentCondition(10_000_000, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + // 284 is 3 standard deviations for 100k trials with 10% probability. + int tolerance = 284; + + int truthyAssignments = evaluateRandomAssignments(oneOfConditionPerCondition, 100000); + + // Evaluate less than or equal 10% to approx 10% + assertTrue(truthyAssignments >= 10_000 - tolerance); + assertTrue(truthyAssignments <= 10_000 + tolerance); + } + + @Test + public void testEvaluateConditionsBetweenApproximateToTrue() { + // Micropercent range is 40% to 60%. + OneOfCondition oneOfConditionPerCondition = createBetweenPercentCondition(40_000_000, + 60_000_000, "seed"); + // 379 is 3 standard deviations for 100k trials with 20% probability. + int tolerance = 379; + + int truthyAssignments = evaluateRandomAssignments(oneOfConditionPerCondition, 100000); + + // Evaluate between 40% to 60% to approx 20% + assertTrue(truthyAssignments >= 20_000 - tolerance); + assertTrue(truthyAssignments <= 20_000 + tolerance); + } + + @Test + public void testEvaluateConditionsInterquartileToFiftyPercent() { + // Micropercent range is 25% to 75%. + OneOfCondition oneOfConditionPerCondition = createBetweenPercentCondition(25_000_000, + 75_000_000, "seed"); + // 474 is 3 standard deviations for 100k trials with 50% probability. + int tolerance = 474; + + int truthyAssignments = evaluateRandomAssignments(oneOfConditionPerCondition, 100000); + + // Evaluate between 25% to 75 to approx 50% + assertTrue(truthyAssignments >= 50_000 - tolerance); + assertTrue(truthyAssignments <= 50_000 + tolerance); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.01"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalInvalidValueNumericOperationToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("non-numeric")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.01"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-49.9"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericEqualsToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericEqualsToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.000001"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericNotEqualsToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_NOT_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericNotEqualsToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_NOT_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.000001"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericGreaterEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericGreaterEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.01"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericgreaterThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericGreaterThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-49.09"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hudred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringDoesNotContainToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_DOES_NOT_CONTAIN, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hudred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringDoesNotContainToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_DOES_NOT_CONTAIN, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringExactlyMatchesToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_EXACTLY_MATCHES, ImmutableList.of("hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringExactlyMatchesToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_EXACTLY_MATCHES, ImmutableList.of("hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsRegexToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of(".*hund.*")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsRegexToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of("$hund.*")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two ahundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsInvalidRegexToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of("abc)")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two ahundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.2.0.1"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessThanInvalidVersionToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.-20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.2.0.1"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL, ImmutableList.of("50.0.20.0.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL, ImmutableList.of("50.0.2")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.2.1.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN, ImmutableList.of("50.0.2")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.1"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL, ImmutableList.of("50.0.20.1")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_EQUAL, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_EQUAL, ImmutableList.of("50.0.20.1")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticNotEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL, ImmutableList.of("50.0.20.1")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticNotEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + private ServerCondition createCustomSignalServerCondition( + CustomSignalOperator operator, + ImmutableList targetCustomSignalValues) { + CustomSignalCondition condition = new CustomSignalCondition("signal_key", operator, + targetCustomSignalValues); + OneOfCondition oneOfConditionCustomSignal = new OneOfCondition(); + oneOfConditionCustomSignal.setCustomSignal(condition); + return new ServerCondition("signal_key", oneOfConditionCustomSignal); + } + + private int evaluateRandomAssignments(OneOfCondition percentCondition, int numOfAssignments) { + int evalTrueCount = 0; + ServerCondition condition = new ServerCondition("is_enabled", percentCondition); + for (int i = 0; i < numOfAssignments; i++) { + UUID randomizationId = UUID.randomUUID(); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", randomizationId.toString()); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + if (result.get("is_enabled")) { + evalTrueCount++; + } + } + return evalTrueCount; + } + + private OneOfCondition createPercentCondition(Integer microPercent, + PercentConditionOperator operator, String seed) { + PercentCondition percentCondition = new PercentCondition(microPercent, operator, seed); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setPercent(percentCondition); + return oneOfCondition; + } + + private OneOfCondition createBetweenPercentCondition(Integer lowerBound, Integer upperBound, + String seed) { + MicroPercentRange microPercentRange = new MicroPercentRange(lowerBound, upperBound); + PercentCondition percentCondition = new PercentCondition(microPercentRange, + PercentConditionOperator.BETWEEN, seed); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setPercent(percentCondition); + return oneOfCondition; + } + + private OneOfCondition createOneOfOrCondition(OneOfCondition condition) { + OrCondition orCondition = condition != null ? new OrCondition(ImmutableList.of(condition)) + : new OrCondition(ImmutableList.of()); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setOrCondition(orCondition); + return oneOfCondition; + } + + private OneOfCondition createOneOfAndCondition(OneOfCondition condition) { + AndCondition andCondition = condition != null ? new AndCondition(ImmutableList.of(condition)) + : new AndCondition(ImmutableList.of()); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setAndCondition(andCondition); + return oneOfCondition; + } + + private OneOfCondition createOneOfTrueCondition() { + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setTrue(); + return oneOfCondition; + } + + private OneOfCondition createOneOfFalseCondition() { + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setFalse(); + return oneOfCondition; + } +} \ No newline at end of file diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index edc52a19d..78a5c276a 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -25,7 +25,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; @@ -48,82 +47,165 @@ import com.google.firebase.remoteconfig.internal.TemplateResponse; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; - +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.Before; import org.junit.Test; public class FirebaseRemoteConfigClientImplTest { private static final String TEST_REMOTE_CONFIG_URL = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + private static final String TEST_SERVER_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; private static final List HTTP_STATUS_CODES = ImmutableList.of(401, 404, 500); - private static final Map HTTP_STATUS_TO_ERROR_CODE = ImmutableMap.of( + private static final Map HTTP_STATUS_TO_ERROR_CODE = + ImmutableMap.of( 401, ErrorCode.UNAUTHENTICATED, 404, ErrorCode.NOT_FOUND, 500, ErrorCode.INTERNAL); - private static final String MOCK_TEMPLATE_RESPONSE = TestUtils - .loadResource("getRemoteConfig.json"); + private static final String MOCK_TEMPLATE_RESPONSE = + TestUtils.loadResource("getRemoteConfig.json"); + + private static final String MOCK_SERVER_TEMPLATE_RESPONSE = + TestUtils.loadResource("getServerRemoteConfig.json"); - private static final String MOCK_LIST_VERSIONS_RESPONSE = TestUtils - .loadResource("listRemoteConfigVersions.json"); + private static final String MOCK_LIST_VERSIONS_RESPONSE = + TestUtils.loadResource("listRemoteConfigVersions.json"); private static final String TEST_ETAG = "etag-123456789012-1"; - private static final Map EXPECTED_PARAMETERS = ImmutableMap.of( - "welcome_message_text", new Parameter() - .setDefaultValue(ParameterValue.of("welcome to app")) - .setConditionalValues(ImmutableMap.of( - "ios_en", ParameterValue.of("welcome to app en") - )) - .setDescription("text for welcome message!") - .setValueType(ParameterValueType.STRING), - "header_text", new Parameter() - .setDefaultValue(ParameterValue.inAppDefault()) - .setValueType(ParameterValueType.STRING) - ); - - private static final Map EXPECTED_PARAMETER_GROUPS = ImmutableMap.of( - "new menu", new ParameterGroup() - .setDescription("New Menu") - .setParameters(ImmutableMap.of( - "pumpkin_spice_season", new Parameter() - .setDefaultValue(ParameterValue.of("true")) - .setDescription("Whether it's currently pumpkin spice season.") - .setValueType(ParameterValueType.BOOLEAN) - ) - ) - ); - - private static final List EXPECTED_CONDITIONS = ImmutableList.of( + private static final Map EXPECTED_PARAMETERS = + ImmutableMap.of( + "welcome_message_text", + new Parameter() + .setDefaultValue(ParameterValue.of("welcome to app")) + .setConditionalValues( + ImmutableMap.of( + "ios_en", ParameterValue.of("welcome to app en"))) + .setDescription("text for welcome message!") + .setValueType(ParameterValueType.STRING), + "header_text", + new Parameter() + .setDefaultValue(ParameterValue.inAppDefault()) + .setValueType(ParameterValueType.STRING)); + + private static final Map EXPECTED_PARAMETER_GROUPS = + ImmutableMap.of( + "new menu", + new ParameterGroup() + .setDescription("New Menu") + .setParameters( + ImmutableMap.of( + "pumpkin_spice_season", + new Parameter() + .setDefaultValue(ParameterValue.of("true")) + .setDescription("Whether it's currently pumpkin spice season.") + .setValueType(ParameterValueType.BOOLEAN)))); + + private static final List EXPECTED_CONDITIONS = + ImmutableList.of( new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']") - .setTagColor(TagColor.INDIGO), - new Condition("android_en", - "device.os == 'android' && device.country in ['us', 'uk']") - ); - - private static final Version EXPECTED_VERSION = new Version(new TemplateResponse.VersionResponse() - .setVersionNumber("17") - .setUpdateOrigin("ADMIN_SDK_NODE") - .setUpdateType("INCREMENTAL_UPDATE") - .setUpdateUser(new TemplateResponse.UserResponse() - .setEmail("firebase-user@account.com") - .setName("dev-admin") - .setImageUrl("http://image.jpg")) - .setUpdateTime("2020-11-15T06:57:26.342763941Z") - .setDescription("promo config") - ); - - private static final Template EXPECTED_TEMPLATE = new Template() + .setTagColor(TagColor.INDIGO), + new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']")); + + private static final List EXPECTED_SERVER_CONDITIONS = + ImmutableList.of( + new ServerCondition("custom_signal", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100"))))))))))), + new ServerCondition("percent", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange( + 12000000, 100000000), + PercentConditionOperator.BETWEEN, + "3maarirs9xzs"))))))))), + new ServerCondition("chained_conditions", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "premium users", + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>( + ImmutableList.of("20")))), + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange( + 25000000, 100000000), + PercentConditionOperator.BETWEEN, + "cla24qoibb61")))))))))); + + private static final Version EXPECTED_VERSION = + new Version( + new TemplateResponse.VersionResponse() + .setVersionNumber("17") + .setUpdateOrigin("ADMIN_SDK_NODE") + .setUpdateType("INCREMENTAL_UPDATE") + .setUpdateUser( + new TemplateResponse.UserResponse() + .setEmail("firebase-user@account.com") + .setName("dev-admin") + .setImageUrl("http://image.jpg")) + .setUpdateTime("2020-11-15T06:57:26.342763941Z") + .setDescription("promo config")); + + private static final Template EXPECTED_TEMPLATE = + new Template() .setETag(TEST_ETAG) .setParameters(EXPECTED_PARAMETERS) .setConditions(EXPECTED_CONDITIONS) @@ -158,16 +240,19 @@ public void testGetTemplate() throws Exception { @Test public void testGetTemplateWithTimestampUpToNanosecondPrecision() throws Exception { - List timestamps = ImmutableList.of( + List timestamps = + ImmutableList.of( "2020-11-15T06:57:26.342Z", "2020-11-15T06:57:26.342763Z", - "2020-11-15T06:57:26.342763941Z" - ); + "2020-11-15T06:57:26.342763941Z"); for (String timestamp : timestamps) { response.addHeader("etag", TEST_ETAG); - String templateResponse = "{\"version\": {" + String templateResponse = + "{\"version\": {" + " \"versionNumber\": \"17\"," - + " \"updateTime\": \"" + timestamp + "\"" + + " \"updateTime\": \"" + + timestamp + + "\"" + " }}"; response.setContent(templateResponse); @@ -221,8 +306,12 @@ public void testGetTemplateHttpError() { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -237,8 +326,8 @@ public void testGetTemplateTransportError() { fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); - assertEquals("Unknown error while making a remote service call: transport error", - error.getMessage()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -271,8 +360,12 @@ public void testGetTemplateErrorWithZeroContentResponse() { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -287,8 +380,12 @@ public void testGetTemplateErrorWithMalformedResponse() { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -297,15 +394,17 @@ public void testGetTemplateErrorWithMalformedResponse() { @Test public void testGetTemplateErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -314,17 +413,22 @@ public void testGetTemplateErrorWithDetails() { @Test public void testGetTemplateErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -339,8 +443,9 @@ public void testGetTemplateAtVersionWithNullString() throws Exception { @Test public void testGetTemplateAtVersionWithInvalidString() throws Exception { - List invalidVersionStrings = ImmutableList - .of("", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = + ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -407,8 +512,12 @@ public void testGetTemplateAtVersionHttpError() { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -423,8 +532,8 @@ public void testGetTemplateAtVersionTransportError() { fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); - assertEquals("Unknown error while making a remote service call: transport error", - error.getMessage()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -457,8 +566,12 @@ public void testGetTemplateAtVersionErrorWithZeroContentResponse() { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -473,8 +586,12 @@ public void testGetTemplateAtVersionErrorWithMalformedResponse() { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -483,15 +600,17 @@ public void testGetTemplateAtVersionErrorWithMalformedResponse() { @Test public void testGetTemplateAtVersionErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -500,17 +619,22 @@ public void testGetTemplateAtVersionErrorWithDetails() { @Test public void testGetTemplateAtVersionErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -553,7 +677,8 @@ public void testPublishTemplateWithValidTemplateAndForceTrue() throws Exception public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exception { response.addHeader("etag", TEST_ETAG); response.setContent(MOCK_TEMPLATE_RESPONSE); - Template expectedTemplate = new Template() + Template expectedTemplate = + new Template() .setETag("etag-123456789012-45") .setParameters(EXPECTED_PARAMETERS) .setConditions(EXPECTED_CONDITIONS) @@ -562,12 +687,13 @@ public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exc Template validatedTemplate = client.publishTemplate(expectedTemplate, true, false); - // check if the etag matches the input template's etag and not the etag from the server response + // check if the etag matches the input template's etag and not the etag from the + // server response assertNotEquals(TEST_ETAG, validatedTemplate.getETag()); assertEquals("etag-123456789012-45", validatedTemplate.getETag()); assertEquals(expectedTemplate, validatedTemplate); - checkPutRequestHeader(interceptor.getLastRequest(), "?validateOnly=true", - "etag-123456789012-45"); + checkPutRequestHeader( + interceptor.getLastRequest(), "?validateOnly=true", "etag-123456789012-45"); } @Test @@ -611,8 +737,12 @@ public void testPublishTemplateHttpError() { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -627,8 +757,8 @@ public void testPublishTemplateTransportError() { fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); - assertEquals("Unknown error while making a remote service call: transport error", - error.getMessage()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -661,8 +791,12 @@ public void testPublishTemplateErrorWithZeroContentResponse() { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -677,8 +811,12 @@ public void testPublishTemplateErrorWithMalformedResponse() { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -687,15 +825,17 @@ public void testPublishTemplateErrorWithMalformedResponse() { @Test public void testPublishTemplateErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -704,17 +844,22 @@ public void testPublishTemplateErrorWithDetails() { @Test public void testPublishTemplateErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -729,8 +874,9 @@ public void testRollbackWithNullString() throws Exception { @Test public void testRollbackWithInvalidString() throws Exception { - List invalidVersionStrings = ImmutableList - .of("", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = + ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -754,8 +900,8 @@ public void testRollbackWithValidString() throws Exception { assertEquals(EXPECTED_TEMPLATE, rolledBackTemplate); assertEquals(1605423446000L, rolledBackTemplate.getVersion().getUpdateTime()); checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } @Test @@ -771,8 +917,8 @@ public void testRollbackWithEmptyTemplateResponse() throws Exception { assertEquals(0, template.getParameterGroups().size()); assertNull(template.getVersion()); checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } @Test(expected = IllegalStateException.class) @@ -801,12 +947,16 @@ public void testRollbackHttpError() throws IOException { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.POST); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @@ -819,8 +969,8 @@ public void testRollbackTransportError() { fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); - assertEquals("Unknown error while making a remote service call: transport error", - error.getMessage()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -842,8 +992,8 @@ public void testRollbackSuccessResponseWithUnexpectedPayload() throws IOExceptio assertNull(error.getRemoteConfigErrorCode()); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } @Test @@ -855,12 +1005,16 @@ public void testRollbackErrorWithZeroContentResponse() throws IOException { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.POST); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @@ -873,52 +1027,63 @@ public void testRollbackErrorWithMalformedResponse() throws IOException { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.POST); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @Test public void testRollbackErrorWithDetails() throws IOException { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.POST); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @Test public void testRollbackErrorWithRcError() throws IOException { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.POST); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @@ -940,33 +1105,36 @@ public void testListVersionsWithNullOptions() throws Exception { public void testListVersionsWithOptions() throws Exception { response.setContent(MOCK_LIST_VERSIONS_RESPONSE); - TemplateResponse.ListVersionsResponse versionsList = client.listVersions( + TemplateResponse.ListVersionsResponse versionsList = + client.listVersions( ListVersionsOptions.builder() - .setPageSize(10) - .setPageToken("token") - .setStartTimeMillis(1605219122000L) - .setEndTimeMillis(1606245035000L) - .setEndVersionNumber("29").build()); + .setPageSize(10) + .setPageToken("token") + .setStartTimeMillis(1605219122000L) + .setEndTimeMillis(1606245035000L) + .setEndVersionNumber("29") + .build()); assertTrue(versionsList.hasVersions()); HttpRequest request = interceptor.getLastRequest(); - String urlWithoutParameters = request.getUrl().toString() - .substring(0, request.getUrl().toString().lastIndexOf('?')); - final Map expectedQuery = ImmutableMap.of( + String urlWithoutParameters = + request.getUrl().toString().substring(0, request.getUrl().toString().lastIndexOf('?')); + final Map expectedQuery = + ImmutableMap.of( "endVersionNumber", "29", "pageSize", "10", "pageToken", "token", "startTime", "2020-11-12T22:12:02.000000000Z", - "endTime", "2020-11-24T19:10:35.000000000Z" - ); + "endTime", "2020-11-24T19:10:35.000000000Z"); Map actualQuery = new HashMap<>(); String query = request.getUrl().toURI().getQuery(); String[] pairs = query.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); - actualQuery.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), - URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + actualQuery.put( + URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); } assertEquals("GET", request.getRequestMethod()); @@ -998,8 +1166,12 @@ public void testListVersionsHttpError() { client.listVersions(null); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1014,8 +1186,8 @@ public void testListVersionsTransportError() { fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); - assertEquals("Unknown error while making a remote service call: transport error", - error.getMessage()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -1048,8 +1220,12 @@ public void testListVersionsErrorWithZeroContentResponse() { client.listVersions(null); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1064,8 +1240,12 @@ public void testListVersionsErrorWithMalformedResponse() { client.listVersions(null); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, - "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1074,15 +1254,17 @@ public void testListVersionsErrorWithMalformedResponse() { @Test public void testListVersionsErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.listVersions(null); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1091,17 +1273,22 @@ public void testListVersionsErrorWithDetails() { @Test public void testListVersionsErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.listVersions(null); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1126,7 +1313,8 @@ public void testBuilderNullRequestFactory() { @Test public void testFromApp() throws IOException { - FirebaseOptions options = FirebaseOptions.builder() + FirebaseOptions options = + FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -1138,8 +1326,8 @@ public void testFromApp() throws IOException { assertEquals(TEST_REMOTE_CONFIG_URL, client.getRemoteConfigUrl()); assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = client.getRequestFactory().buildGetRequest( - new GenericUrl("https://example.com")); + HttpRequest request = + client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com")); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } finally { app.delete(); @@ -1147,33 +1335,32 @@ public void testFromApp() throws IOException { } private FirebaseRemoteConfigClientImpl initRemoteConfigClient( - MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(mockResponse) - .build(); + MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { + MockHttpTransport transport = + new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); return FirebaseRemoteConfigClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(transport.createRequestFactory()) - .setResponseInterceptor(interceptor) - .build(); + .setProjectId("test-project") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); } private FirebaseRemoteConfigClientImpl initClientWithFaultyTransport() { HttpTransport transport = TestUtils.createFaultyHttpTransport(); return FirebaseRemoteConfigClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(transport.createRequestFactory()) - .build(); + .setProjectId("test-project") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .build(); } private FirebaseRemoteConfigClientImpl.Builder fullyPopulatedBuilder() { return FirebaseRemoteConfigClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); + .setProjectId("test-project") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); } private void checkGetRequestHeader(HttpRequest request) { @@ -1189,6 +1376,19 @@ private void checkGetRequestHeader(HttpRequest request, String urlSuffix) { assertEquals("gzip", headers.getAcceptEncoding()); } + private void checkGetRequestHeaderForServer(HttpRequest request) { + checkGetRequestHeaderForServer(request, ""); + } + + private void checkGetRequestHeaderForServer(HttpRequest request, String urlSuffix) { + assertEquals("GET", request.getRequestMethod()); + assertEquals(TEST_SERVER_REMOTE_CONFIG_URL + urlSuffix, request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); + assertEquals("gzip", headers.getAcceptEncoding()); + } + private void checkPutRequestHeader(HttpRequest request) { checkPutRequestHeader(request, "", TEST_ETAG); } @@ -1212,8 +1412,8 @@ private void checkPostRequestHeader(HttpRequest request, String urlSuffix) { assertEquals("gzip", headers.getAcceptEncoding()); } - private void checkRequestContent( - HttpRequest request, Map expected) throws IOException { + private void checkRequestContent(HttpRequest request, Map expected) + throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); JsonParser parser = ApiClientUtils.getDefaultJsonFactory().createJsonParser(out.toString()); @@ -1223,11 +1423,11 @@ private void checkRequestContent( } private void checkExceptionFromHttpResponse( - FirebaseRemoteConfigException error, - ErrorCode expectedCode, - RemoteConfigErrorCode expectedRemoteConfigCode, - String expectedMessage, - String httpMethod) { + FirebaseRemoteConfigException error, + ErrorCode expectedCode, + RemoteConfigErrorCode expectedRemoteConfigCode, + String expectedMessage, + String httpMethod) { assertEquals(expectedCode, error.getErrorCode()); assertEquals(expectedMessage, error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); @@ -1238,4 +1438,225 @@ private void checkExceptionFromHttpResponse( assertEquals(httpMethod, request.getMethod()); assertTrue(request.getUrl().startsWith("https://firebaseremoteconfig.googleapis.com")); } + + // Get server template tests + + @Test + public void testGetServerTemplate() throws Exception { + response.addHeader("etag", TEST_ETAG); + response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE); + + String receivedTemplate = client.getServerTemplate(); + ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); + + assertEquals(EXPECTED_PARAMETERS, serverTemplateData.getParameters()); + assertEquals(TEST_ETAG, serverTemplateData.getETag()); + assertEquals( + convertObjectToString(EXPECTED_SERVER_CONDITIONS), + convertObjectToString(serverTemplateData.getServerConditions())); + assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + + @Test + public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws Exception { + List timestamps = + ImmutableList.of( + "2020-11-15T06:57:26.342Z", + "2020-11-15T06:57:26.342763Z", + "2020-11-15T06:57:26.342763941Z"); + for (String timestamp : timestamps) { + response.addHeader("etag", TEST_ETAG); + String templateResponse = + "{\"version\": {" + + " \"versionNumber\": \"17\"," + + " \"updateTime\": \"" + + timestamp + + "\"" + + " }}"; + response.setContent(templateResponse); + + String receivedTemplate = client.getServerTemplate(); + ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); + assertEquals(TEST_ETAG, serverTemplateData.getETag()); + assertEquals("17", serverTemplateData.getVersion().getVersionNumber()); + assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateWithEmptyTemplateResponse() throws Exception { + response.addHeader("etag", TEST_ETAG); + response.setContent("{}"); + + String receivedTemplate = client.getServerTemplate(); + ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); + + assertEquals(TEST_ETAG, serverTemplateData.getETag()); + assertEquals(0, serverTemplateData.getParameters().size()); + assertEquals(0, serverTemplateData.getServerConditions().size()); + assertEquals(0, serverTemplateData.getParameterGroups().size()); + assertNull(serverTemplateData.getVersion()); + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + + @Test(expected = IllegalStateException.class) + public void testGetServerTemplateWithNoEtag() throws FirebaseRemoteConfigException { + // ETag does not exist + response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE); + + client.getServerTemplate(); + } + + @Test(expected = IllegalStateException.class) + public void testGetServerTemplateWithEmptyEtag() throws FirebaseRemoteConfigException { + // Empty ETag + response.addHeader("etag", ""); + response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE); + + client.getServerTemplate(); + } + + @Test + public void testGetServerTemplateHttpError() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("{}"); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateTransportError() { + client = initClientWithFaultyTransport(); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getRemoteConfigErrorCode()); + } + } + + @Test + public void testGetServerTemplateSuccessResponseWithUnexpectedPayload() { + response.setContent("not valid json"); + + try { + client.getServerTemplate(); + fail("No error thrown for malformed response"); + } catch (FirebaseRemoteConfigException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); + assertNotNull(error.getCause()); + assertNotNull(error.getHttpResponse()); + assertNull(error.getRemoteConfigErrorCode()); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + + @Test + public void testGetServerTemplateErrorWithZeroContentResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setZeroContent(); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateErrorWithMalformedResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("not json"); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateErrorWithDetails() { + for (int code : HTTP_STATUS_CODES) { + response + .setStatusCode(code) + .setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateErrorWithRcError() { + for (int code : HTTP_STATUS_CODES) { + response + .setStatusCode(code) + .setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + public static String convertObjectToString(Object object) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); // Optional: pretty printing + return gson.toJson(object); + } } diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index d3e7fbff2..8b416b67b 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -27,22 +27,26 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; - import com.google.firebase.remoteconfig.internal.TemplateResponse; - +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import java.util.concurrent.ExecutionException; - import org.junit.After; import org.junit.Test; + +/** Tests +* for {@link FirebaseRemoteConfig}. +* */ public class FirebaseRemoteConfigTest { - private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); private static final FirebaseRemoteConfigException TEST_EXCEPTION = - new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); + new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @After public void tearDown() { @@ -76,10 +80,25 @@ public void testDefaultRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } + @Test + public void testDefaultServerRemoteConfigClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app); + + FirebaseRemoteConfigClient client = remoteConfig.getRemoteConfigClient(); + + assertTrue(client instanceof FirebaseRemoteConfigClientImpl); + assertSame(client, remoteConfig.getRemoteConfigClient()); + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); + } + @Test public void testAppDelete() { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); @@ -98,16 +117,16 @@ public void testAppDelete() { @Test public void testRemoteConfigClientWithoutProjectId() { - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .build(); + FirebaseOptions options = + FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); FirebaseApp.initializeApp(options); try { FirebaseRemoteConfig.getInstance(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - String message = "Project ID is required to access Remote Config service. Use a service " + String message = + "Project ID is required to access Remote Config service. Use a service " + "account credential or set the project ID explicitly via FirebaseOptions. " + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + "environment variable."; @@ -121,8 +140,8 @@ public void testRemoteConfigClientWithoutProjectId() { @Test public void testGetTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplate(); @@ -144,8 +163,8 @@ public void testGetTemplateFailure() { @Test public void testGetTemplateAsync() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAsync().get(); @@ -169,8 +188,8 @@ public void testGetTemplateAsyncFailure() throws InterruptedException { @Test public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion("64"); @@ -192,8 +211,8 @@ public void testGetTemplateAtVersionWithStringValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync("55").get(); @@ -215,8 +234,8 @@ public void testGetTemplateAtVersionAsyncWithStringValueFailure() throws Interru @Test public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion(64L); @@ -238,8 +257,8 @@ public void testGetTemplateAtVersionWithLongValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync(55L).get(); @@ -407,8 +426,8 @@ public void testForcePublishTemplateAsyncFailure() throws InterruptedException { @Test public void testRollbackWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback("64"); @@ -430,8 +449,8 @@ public void testRollbackWithStringValueFailure() { @Test public void testRollbackAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync("55").get(); @@ -453,8 +472,8 @@ public void testRollbackAsyncWithStringValueFailure() throws InterruptedExceptio @Test public void testRollbackWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback(64L); @@ -476,8 +495,8 @@ public void testRollbackWithLongValueFailure() { @Test public void testRollbackAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync(55L).get(); @@ -501,7 +520,8 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException @Test public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); @@ -524,7 +544,8 @@ public void testListVersionsWithNoOptionsFailure() { @Test public void testListVersionsAsyncWithNoOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); @@ -547,12 +568,13 @@ public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedExcept @Test public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersions( - ListVersionsOptions.builder().build()); + ListVersionsPage listVersionsPage = + remoteConfig.listVersions(ListVersionsOptions.builder().build()); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -571,12 +593,13 @@ public void testListVersionsWithOptionsFailure() { @Test public void testListVersionsAsyncWithOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync( - ListVersionsOptions.builder().build()).get(); + ListVersionsPage listVersionsPage = + remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -597,4 +620,62 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); return new FirebaseRemoteConfig(app, client); } + + // Get Server template tests + + @Test + public void testGetServerTemplate() throws FirebaseRemoteConfigException { + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ServerTemplate template = remoteConfig.getServerTemplate(); + String templateData = template.toJson(); + JsonElement expectedJson = + JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement actualJson = JsonParser.parseString(templateData); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testGetServerTemplateFailure() { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.getServerTemplate(); + } catch (FirebaseRemoteConfigException e) { + assertSame(TEST_EXCEPTION, e); + } + } + + @Test + public void testGetServerTemplateAsync() throws Exception { + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); + String templateData = template.toJson(); + JsonElement expectedJson = + JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement actualJson = JsonParser.parseString(templateData); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testGetServerTemplateAsyncFailure() throws InterruptedException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.getServerTemplateAsync().get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } } diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java index 9ca58508d..3ac7f6b1c 100644 --- a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java +++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java @@ -21,28 +21,35 @@ public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{ private final Template resultTemplate; + private final String resultServerTemplate; private final FirebaseRemoteConfigException exception; private final ListVersionsResponse listVersionsResponse; private MockRemoteConfigClient(Template resultTemplate, - ListVersionsResponse listVersionsResponse, - FirebaseRemoteConfigException exception) { + String resultServerTemplate, + ListVersionsResponse listVersionsResponse, + FirebaseRemoteConfigException exception) { this.resultTemplate = resultTemplate; + this.resultServerTemplate = resultServerTemplate; this.listVersionsResponse = listVersionsResponse; this.exception = exception; } static MockRemoteConfigClient fromTemplate(Template resultTemplate) { - return new MockRemoteConfigClient(resultTemplate, null, null); + return new MockRemoteConfigClient(resultTemplate,null, null, null); + } + + static MockRemoteConfigClient fromServerTemplate(String resultServerTemplate) { + return new MockRemoteConfigClient(null, resultServerTemplate,null, null); } static MockRemoteConfigClient fromListVersionsResponse( ListVersionsResponse listVersionsResponse) { - return new MockRemoteConfigClient(null, listVersionsResponse, null); + return new MockRemoteConfigClient(null,null, listVersionsResponse, null); } static MockRemoteConfigClient fromException(FirebaseRemoteConfigException exception) { - return new MockRemoteConfigClient(null, null, exception); + return new MockRemoteConfigClient(null,null, null, exception); } @Override @@ -53,6 +60,14 @@ public Template getTemplate() throws FirebaseRemoteConfigException { return resultTemplate; } + @Override + public String getServerTemplate() throws FirebaseRemoteConfigException { + if (exception != null) { + throw exception; + } + return resultServerTemplate; + } + @Override public Template getTemplateAtVersion(String versionNumber) throws FirebaseRemoteConfigException { if (exception != null) { diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java new file mode 100644 index 000000000..6cbece1c0 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.CustomSignalConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.ServerConditionResponse; +import java.util.ArrayList; +import org.junit.Test; + +/** Tests +* for {@link ServerCondition}. +* */ +public class ServerConditionTest { + + @Test + public void testConstructor() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition serverCondition = new ServerCondition("ios_en_1", conditions); + + assertEquals("ios_en_1", serverCondition.getName()); + assertEquals(conditions, serverCondition.getCondition()); + } + + @Test + public void testConstructorWithResponse() { + CustomSignalConditionResponse customResponse = + new CustomSignalConditionResponse() + .setKey("test_key") + .setOperator("NUMERIC_EQUAL") + .setTargetValues(ImmutableList.of("1")); + OneOfConditionResponse conditionResponse = + new OneOfConditionResponse().setCustomSignalCondition(customResponse); + ServerConditionResponse response = + new ServerConditionResponse().setName("ios_en_2").setServerCondition(conditionResponse); + ServerCondition serverCondition = new ServerCondition(response); + + assertEquals("ios_en_2", serverCondition.getName()); + assertEquals("test_key", serverCondition.getCondition().getCustomSignal().getCustomSignalKey()); + } + + @Test + public void testIllegalConstructor() { + IllegalArgumentException error = + assertThrows(IllegalArgumentException.class, () -> new ServerCondition(null, null)); + + assertEquals("condition name must not be null or empty", error.getMessage()); + } + + @Test + public void testConstructorWithNullServerConditionResponse() { + assertThrows(NullPointerException.class, () -> new ServerCondition(null)); + } + + @Test + public void testSetNullName() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition condition = new ServerCondition("ios", conditions); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> condition.setName(null)); + + assertEquals("condition name must not be null or empty", error.getMessage()); + } + + @Test + public void testSetEmptyName() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition condition = new ServerCondition("ios", conditions); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> condition.setName("")); + + assertEquals("condition name must not be null or empty", error.getMessage()); + } + + @Test + public void testSetNullServerCondition() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition condition = new ServerCondition("ios", conditions); + + assertThrows(NullPointerException.class, () -> condition.setServerCondition(null)); + } + + @Test + public void testEquality() { + OneOfCondition conditionOne = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + OneOfCondition conditionTwo = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>(ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>(ImmutableList.of("20")))), + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange(25000000, 100000000), + PercentConditionOperator.BETWEEN, + "cla24qoibb61")))))))); + + final ServerCondition serverConditionOne = new ServerCondition("ios", conditionOne); + final ServerCondition serverConditionTwo = new ServerCondition("ios", conditionOne); + final ServerCondition serverConditionThree = new ServerCondition("android", conditionTwo); + final ServerCondition serverConditionFour = new ServerCondition("android", conditionTwo); + + assertEquals(serverConditionOne, serverConditionTwo); + assertEquals(serverConditionThree, serverConditionFour); + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java new file mode 100644 index 000000000..bafe84331 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -0,0 +1,421 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.api.core.ApiFuture; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestUtils; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.junit.BeforeClass; +import org.junit.Test; + +/** Tests +* for {@link ServerTemplateImpl}. +* */ +public class ServerTemplateImplTest { + + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + + private static String cacheTemplate; + + @BeforeClass + public static void setUpClass() { + cacheTemplate = TestUtils.loadResource("getServerTemplateData.json"); + } + + @Test + public void testServerTemplateWithoutCacheValueThrowsException() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + + IllegalArgumentException error = + assertThrows( + IllegalArgumentException.class, + () -> new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build()); + + assertEquals("JSON String must not be null or empty.", error.getMessage()); + } + + @Test + public void testEvaluateWithoutContextReturnsDefaultValue() throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalReturnsDefaultValue() throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "100").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalReturnsConditionalValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalWithoutContextReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalWithInvalidContextReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "abc").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluatePercentWithoutRandomizationIdReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Percent")); + } + + @Test + public void testEvaluatePercentReturnsConditionalValue() throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value", evaluatedConfig.getString("Percent")); + } + + @Test + public void testEvaluateWithoutDefaultValueReturnsEmptyString() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("", evaluatedConfig.getString("Unset default value")); + } + + @Test + public void testEvaluateWithInvalidCacheValueThrowsException() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + String invalidJsonString = "abc"; + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(invalidJsonString) + .build(); + + FirebaseRemoteConfigException error = + assertThrows(FirebaseRemoteConfigException.class, () -> template.evaluate(context)); + + assertEquals( + "No Remote Config Server template in cache. Call load() before " + "calling evaluate().", + error.getMessage()); + } + + @Test + public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("", evaluatedConfig.getString("In-app default")); + } + + @Test + public void testEvaluateWithDerivedInAppDefaultReturnsDefaultValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Derived in-app default")); + } + + @Test + public void testEvaluateWithMultipleConditionReturnsConditionalValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value 1", evaluatedConfig.getString("Multiple conditions")); + } + + @Test + public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = + new KeysAndValues.Builder() + .put("users", "100") + .put("premium users", 20) + .put("randomizationId", "user") + .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Chained conditions")); + } + + @Test + public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = + new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value", evaluatedConfig.getString("Chained conditions")); + } + + @Test + public void testGetEvaluateConfigOnInvalidTypeReturnsDefaultValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(0L, evaluatedConfig.getLong("Percent")); + } + + @Test + public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(ValueSource.STATIC, evaluatedConfig.getValueSource("invalid")); + } + + @Test + public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().put("In-app default", "abc").build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(ValueSource.DEFAULT, evaluatedConfig.getValueSource("In-app default")); + } + + @Test + public void testGetEvaluateConfigUnsetDefaultConfigReturnsDefaultValueSource() throws Exception { + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(ValueSource.DEFAULT, evaluatedConfig.getValueSource("Unset default config")); + } + + private static final String TEST_ETAG = "etag-123456789012-1"; + + private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "test-app"); + return new FirebaseRemoteConfig(app, client); + } + + @Test + public void testLoad() throws Exception { + // 1. Define the template data that the mock client will return. + // This is the EXPECTED state after `load()` is called. + final String expectedTemplateJsonAfterLoad = + new ServerTemplateData().setETag(TEST_ETAG).toJSON(); + + // 2. Mock the HTTP client to return the predefined response. + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate(expectedTemplateJsonAfterLoad); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + // 3. Build the template instance. + // It's initialized with a complex `cacheTemplate` to ensure `load()` properly + // overwrites it. + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); + ServerTemplate template = + remoteConfig + .serverTemplateBuilder() + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) // This is the initial state before `load()` + .build(); + + // 4. Call the load method, which fetches the new template from the mock client. + ApiFuture loadFuture = template.load(); + loadFuture.get(); // Wait for the async operation to complete. + + // 5. Get the ACTUAL state of the template after `load()` has executed. + String actualJsonAfterLoad = template.toJson(); + + // 6. Assert that the template's state has been updated to match what the mock + // client returned. + // Parsing to JsonElement performs a deep, order-insensitive comparison. + JsonElement expectedJson = JsonParser.parseString(expectedTemplateJsonAfterLoad); + JsonElement actualJson = JsonParser.parseString(actualJsonAfterLoad); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testBuilderParsesCachedTemplateCorrectly() throws FirebaseRemoteConfigException { + // Arrange: + // 1. Create a canonical JSON string by parsing the input file and then + // re-serializing it. This gives us the precise expected output format, + // accounting for any formatting or default value differences. + ServerTemplateData canonicalData = ServerTemplateData.fromJSON(cacheTemplate); + String expectedJsonString = canonicalData.toJSON(); + + // Act: + // 2. Build a ServerTemplate instance from the original cached JSON string, + // which triggers the parsing logic we want to test. + ServerTemplate template = + new ServerTemplateImpl.Builder(null).cachedTemplate(cacheTemplate).build(); + + // Assert: + // 3. Compare the JSON from the newly built template against the canonical + // version. + // This verifies that the internal state was parsed and stored correctly. + // Using JsonElement ensures the comparison is not affected by key order. + JsonElement expectedJsonTree = JsonParser.parseString(expectedJsonString); + JsonElement actualJsonTree = JsonParser.parseString(template.toJson()); + + assertEquals(expectedJsonTree, actualJsonTree); + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ValueTest.java new file mode 100644 index 000000000..c1822b063 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ValueTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ValueTest { + @Test + public void testGetSourceReturnsValueSource() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.getSource(), ValueSource.STATIC); + } + + @Test + public void testAsStringReturnsValueAsString() { + Value value = new Value(ValueSource.STATIC, "sample-string"); + assertEquals(value.asString(), "sample-string"); + } + + @Test + public void testAsStringReturnsDefaultEmptyString() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.asString(), ""); + } + + @Test + public void testAsLongReturnsDefaultValueForStaticSource() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.asLong(), 0L); + } + + @Test + public void testAsLongReturnsDefaultValueForInvalidSourceValue() { + Value value = new Value(ValueSource.REMOTE, "sample-string"); + assertEquals(value.asLong(), 0L); + } + + @Test + public void testAsLongReturnsSourceValueAsLong() { + Value value = new Value(ValueSource.REMOTE, "123"); + assertEquals(value.asLong(), 123L); + } + + @Test + public void testAsDoubleReturnsDefaultValueForStaticSource() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.asDouble(), 0, 0); + } + + @Test + public void testAsDoubleReturnsDefaultValueForInvalidSourceValue() { + Value value = new Value(ValueSource.REMOTE, "sample-string"); + assertEquals(value.asDouble(), 0, 0); + } + + @Test + public void testAsDoubleReturnsSourceValueAsDouble() { + Value value = new Value(ValueSource.REMOTE, "123.34"); + assertEquals(value.asDouble(), 123.34, 0); + } + + @Test + public void testAsBooleanReturnsDefaultValueForStaticSource() { + Value value = new Value(ValueSource.STATIC); + assertFalse(value.asBoolean()); + } + + @Test + public void testAsBooleanReturnsDefaultValueForInvalidSourceValue() { + Value value = new Value(ValueSource.REMOTE, "sample-string"); + assertFalse(value.asBoolean()); + } + + @Test + public void testAsBooleanReturnsSourceValueAsBoolean() { + Value value = new Value(ValueSource.REMOTE, "1"); + assertTrue(value.asBoolean()); + } + + @Test + public void testAsBooleanReturnsSourceValueYesAsBoolean() { + Value value = new Value(ValueSource.REMOTE, "YeS"); + assertTrue(value.asBoolean()); + } +} + \ No newline at end of file diff --git a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java index 515629660..1e524195f 100644 --- a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import com.google.firebase.remoteconfig.internal.TemplateResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; @@ -27,15 +28,16 @@ public class VersionTest { - @Test(expected = NullPointerException.class) + @Test public void testConstructorWithNullVersionResponse() { - new Version(null); + assertThrows(NullPointerException.class, () -> new Version(null)); } - @Test(expected = IllegalStateException.class) + @Test public void testConstructorWithInvalidUpdateTime() { - new Version(new VersionResponse() - .setUpdateTime("sunday,26th")); + assertThrows(IllegalStateException.class, () -> + new Version(new VersionResponse().setUpdateTime("sunday,26th"))); + } @Test diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json new file mode 100644 index 000000000..e5cb1a9eb --- /dev/null +++ b/src/test/resources/getServerRemoteConfig.json @@ -0,0 +1,142 @@ +{ + "conditions": [ + { + "name": "custom_signal", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + } + ] + } + } + ] + } + } + }, + { + "name": "percent", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "percent": { + "percentOperator": "BETWEEN", + "seed": "3maarirs9xzs", + "microPercentRange": { + "microPercentLowerBound": 12000000, + "microPercentUpperBound": 100000000 + } + } + } + ] + } + } + ] + } + } + }, + { + "name": "chained_conditions", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + }, + { + "customSignal": { + "customSignalOperator": "NUMERIC_GREATER_THAN", + "customSignalKey": "premium users", + "targetCustomSignalValues": [ + "20" + ] + } + }, + { + "percent": { + "percentOperator": "BETWEEN", + "seed": "cla24qoibb61", + "microPercentRange": { + "microPercentLowerBound": 25000000, + "microPercentUpperBound": 100000000 + } + } + } + ] + } + } + ] + } + } + } + ], + "parameters": { + "welcome_message_text": { + "defaultValue": { + "value": "welcome to app" + }, + "conditionalValues": { + "ios_en": { + "value": "welcome to app en" + } + }, + "description": "text for welcome message!", + "valueType": "STRING" + }, + "header_text": { + "defaultValue": { + "useInAppDefault": true + }, + "valueType": "STRING" + } + }, + "parameterGroups": { + "new menu": { + "description": "New Menu", + "parameters": { + "pumpkin_spice_season": { + "defaultValue": { + "value": "true" + }, + "description": "Whether it's currently pumpkin spice season.", + "valueType": "BOOLEAN" + } + } + } + }, + "version": { + "versionNumber": "17", + "updateOrigin": "ADMIN_SDK_NODE", + "updateType": "INCREMENTAL_UPDATE", + "updateUser": { + "email": "firebase-user@account.com", + "name": "dev-admin", + "imageUrl": "http://image.jpg" + }, + "updateTime": "2020-11-15T06:57:26.342763941Z", + "description": "promo config" + } +} \ No newline at end of file diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json new file mode 100644 index 000000000..c26f74185 --- /dev/null +++ b/src/test/resources/getServerTemplateData.json @@ -0,0 +1,181 @@ +{ + "conditions": [ + { + "name": "custom_signal", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + } + ] + } + } + ] + } + } + }, + { + "name": "percent", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "percent": { + "percentOperator": "BETWEEN", + "seed": "3maarirs9xzs", + "microPercentRange": { + "microPercentLowerBound": 12000000, + "microPercentUpperBound": 100000000 + } + } + } + ] + } + } + ] + } + } + }, + { + "name": "chained_conditions", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + }, + { + "customSignal": { + "customSignalOperator": "NUMERIC_GREATER_THAN", + "customSignalKey": "premium users", + "targetCustomSignalValues": [ + "20" + ] + }, + "percent": { + "percentOperator": "BETWEEN", + "seed": "cla24qoibb61", + "microPercentRange": { + "microPercentLowerBound": 25000000, + "microPercentUpperBound": 100000000 + } + } + } + ] + } + } + ] + } + } + } + ], + "parameters": { + "Percent": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "percent": { + "value": "Conditional value" + } + } + }, + "Custom": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "custom_signal": { + "value": "Conditional value" + } + } + }, + "Welcome Message": { + "defaultValue": { + "value": "Welcome" + } + }, + "Unset default value": { + "defaultValue": { + "value": "" + } + }, + "In-app default": { + "defaultValue": { + "useInAppDefault": true + }, + "conditionalValues": { + "percent": { + "value": "Conditional value" + } + } + }, + "Derived in-app default": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "percent": { + "useInAppDefault": true + } + } + }, + "Multiple conditions": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "custom_signal": { + "value": "Conditional value 1" + }, + "percent": { + "value": "Conditional value 2" + } + } + }, + "Chained conditions": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "chained_conditions": { + "value": "Conditional value" + } + } + } + }, + "version": { + "versionNumber": "17", + "updateOrigin": "ADMIN_SDK_NODE", + "updateType": "INCREMENTAL_UPDATE", + "updateUser": { + "email": "firebase-user@account.com", + "name": "dev-admin", + "imageUrl": "http://image.jpg" + }, + "updateTime": "2020-11-15T06:57:26.342763941Z", + "description": "promo config" + } +} \ No newline at end of file From c141def8c6803021e50ec473934d5aa34f187c36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:18:41 -0400 Subject: [PATCH 161/219] chore(deps): Bump org.apache.maven.plugins:maven-surefire-plugin (#1133) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.3 to 3.5.4. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.3...surefire-3.5.4) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 91c203218..30a898622 100644 --- a/pom.xml +++ b/pom.xml @@ -273,7 +273,7 @@ maven-surefire-plugin - 3.5.3 + 3.5.4 ${skipUTs} From 3737fe2fe9cee4190d97bcf5f628787dfd1308c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:54:15 -0400 Subject: [PATCH 162/219] chore(deps): Bump netty.version from 4.2.5.Final to 4.2.6.Final (#1131) Bumps `netty.version` from 4.2.5.Final to 4.2.6.Final. Updates `io.netty:netty-codec-http` from 4.2.5.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.5.Final...netty-4.2.6.Final) Updates `io.netty:netty-handler` from 4.2.5.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.5.Final...netty-4.2.6.Final) Updates `io.netty:netty-transport` from 4.2.5.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.5.Final...netty-4.2.6.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 30a898622..d4a8e3db8 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.5.Final + 4.2.6.Final From 800298c46df2d77eaa089e0cfcfcf48c1b3b19fa Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 24 Sep 2025 11:40:55 -0400 Subject: [PATCH 163/219] [chore] Release 9.7.0 (#1136) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d4a8e3db8..e6fdd2baa 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.6.0 + 9.7.0 jar firebase-admin From 27af418713393604fd82d8022246bf3a3a0944b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:30:38 +0000 Subject: [PATCH 164/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.67.0 to 26.71.0 (#1146) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6fdd2baa..c4998f31b 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.67.0 + 26.71.0 pom import From 1e3bddc2fedcab9cf9b02918b88200097c99648a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:35:14 +0000 Subject: [PATCH 165/219] chore(deps): Bump org.apache.maven.plugins:maven-javadoc-plugin (#1141) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c4998f31b..1c0d9a40a 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ maven-javadoc-plugin - 3.11.3 + 3.12.0 site @@ -294,7 +294,7 @@ maven-javadoc-plugin - 3.11.3 + 3.12.0 attach-javadocs From 88381d8517e7a847b723a3f401cbe01017891abe Mon Sep 17 00:00:00 2001 From: Jos Date: Wed, 19 Nov 2025 16:53:05 +0100 Subject: [PATCH 166/219] fix(fcm): Preserve unmapped `TopicManagementResponse` error reasons (#1073) * RESOURCE_EXHAUSTED is a possible error code as well, given https://developers.google.com/instance-id/reference/server * If the reason is not in the predefined ERROR_CODES it would be more helpful to use the reason String that came in as an parameter than UNKNOWN_ERROR. UNKNOWN_ERROR could be used for null or empty Strings. * For consistency with the current format of the error reasons, logic is added to map them from UPPER_SNAKE_CASE to lower-kebab-case. Entries from the ERROR_CODES that can be computed this way are removed from the map. Extra tests are added for those 3 cases. --- .../messaging/TopicManagementResponse.java | 9 ++-- .../messaging/InstanceIdClientImplTest.java | 50 ++++++++++++++++++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index bbf4c944a..9f75f5871 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -37,10 +37,8 @@ public class TopicManagementResponse { // Server error codes as defined in https://developers.google.com/instance-id/reference/server // TODO: Should we handle other error codes here (e.g. PERMISSION_DENIED)? private static final Map ERROR_CODES = ImmutableMap.builder() - .put("INVALID_ARGUMENT", "invalid-argument") .put("NOT_FOUND", "registration-token-not-registered") .put("INTERNAL", "internal-error") - .put("TOO_MANY_TOPICS", "too-many-topics") .build(); private final int successCount; @@ -101,8 +99,11 @@ public static class Error { private Error(int index, String reason) { this.index = index; - this.reason = ERROR_CODES.containsKey(reason) - ? ERROR_CODES.get(reason) : UNKNOWN_ERROR; + if (reason == null || reason.trim().isEmpty()) { + this.reason = UNKNOWN_ERROR; + } else { + this.reason = ERROR_CODES.getOrDefault(reason, reason.toLowerCase().replace('_', '-')); + } } /** diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index d0151bb94..a6f1c7543 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -404,7 +404,30 @@ public void testTopicManagementResponseWithEmptyList() { @Test public void testTopicManagementResponseErrorToString() { - GenericJson json = new GenericJson().set("error", "test error"); + GenericJson json = new GenericJson().set("error", "INVALID_ARGUMENT"); + ImmutableList jsonList = ImmutableList.of(json); + + TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); + + String expected = "[Error{index=0, reason=invalid-argument}]"; + assertEquals(expected, topicManagementResponse.getErrors().toString()); + } + + @Test + public void testTopicManagementResponseErrorNotInErrorCodes() { + String myError = "MY_ERROR"; + GenericJson json = new GenericJson().set("error", myError); + ImmutableList jsonList = ImmutableList.of(json); + + TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); + + String expected = "[Error{index=0, reason=my-error}]"; + assertEquals(expected, topicManagementResponse.getErrors().toString()); + } + + @Test + public void testTopicManagementResponseErrorUnknown() { + GenericJson json = new GenericJson().set("error", ""); ImmutableList jsonList = ImmutableList.of(json); TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); @@ -413,6 +436,29 @@ public void testTopicManagementResponseErrorToString() { assertEquals(expected, topicManagementResponse.getErrors().toString()); } + @Test + public void testTopicManagementResponseErrorResourceExhausted() { + GenericJson json = new GenericJson().set("error", "RESOURCE_EXHAUSTED"); + ImmutableList jsonList = ImmutableList.of(json); + + TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); + + String expected = "[Error{index=0, reason=resource-exhausted}]"; + assertEquals(expected, topicManagementResponse.getErrors().toString()); + } + + @Test + public void testTopicManagementResponseErrorTooManyTopics() { + GenericJson json = new GenericJson().set("error", "TOO_MANY_TOPICS"); + ImmutableList jsonList = ImmutableList.of(json); + + TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); + + String expected = "[Error{index=0, reason=too-many-topics}]"; + assertEquals(expected, topicManagementResponse.getErrors().toString()); + } + + private static InstanceIdClientImpl initInstanceIdClient( final MockLowLevelHttpResponse mockResponse, final HttpResponseInterceptor interceptor) { @@ -432,7 +478,7 @@ private void checkTopicManagementRequest( assertEquals(1, result.getFailureCount()); assertEquals(1, result.getErrors().size()); assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("unknown-error", result.getErrors().get(0).getReason()); + assertEquals("error-reason", result.getErrors().get(0).getReason()); ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); From 029185574a0028cc4080df607d3f4c2254875979 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:42:37 +0000 Subject: [PATCH 167/219] chore(deps): Bump netty.version from 4.2.6.Final to 4.2.7.Final (#1147) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1c0d9a40a..324dbd935 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.6.Final + 4.2.7.Final From eee14b47ffb7bba7aeb4dafed85f77677f98e937 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:51:37 +0000 Subject: [PATCH 168/219] chore(deps): Bump org.codehaus.mojo:exec-maven-plugin (#1148) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 324dbd935..275a7830d 100644 --- a/pom.xml +++ b/pom.xml @@ -208,7 +208,7 @@ org.codehaus.mojo exec-maven-plugin - 3.5.1 + 3.6.2 test From 31ffeb5a07c8ce519257eca5f406487dd1b862d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:42:15 +0000 Subject: [PATCH 169/219] chore(deps): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1150) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 275a7830d..06d2683fb 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,7 @@ maven-failsafe-plugin - 3.5.3 + 3.5.4 From 901bcf13f664b64eb7ad0b8a7f19803cf07504e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:46:38 -0500 Subject: [PATCH 170/219] chore(deps): Bump org.jacoco:jacoco-maven-plugin from 0.8.13 to 0.8.14 (#1151) Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.13 to 0.8.14. - [Release notes](https://github.com/jacoco/jacoco/releases) - [Commits](https://github.com/jacoco/jacoco/compare/v0.8.13...v0.8.14) --- updated-dependencies: - dependency-name: org.jacoco:jacoco-maven-plugin dependency-version: 0.8.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 06d2683fb..cf3c3900e 100644 --- a/pom.xml +++ b/pom.xml @@ -182,7 +182,7 @@ org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 pre-unit-test From f8f07115c2c1bd86570ec787aeff7c6b4e8bfb67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:54:23 -0500 Subject: [PATCH 171/219] chore(deps): Bump org.apache.maven.plugins:maven-compiler-plugin (#1152) Bumps [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.14.0 to 3.14.1. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.14.0...maven-compiler-plugin-3.14.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.14.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cf3c3900e..18b23fd62 100644 --- a/pom.xml +++ b/pom.xml @@ -263,7 +263,7 @@ maven-compiler-plugin - 3.14.0 + 3.14.1 1.8 1.8 From 7c62e23ade34d458a9445c3ed9f810dd6748d2ac Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 15 Dec 2025 10:50:46 -0500 Subject: [PATCH 172/219] chore: Rename default branch to main (#1156) --- .github/scripts/publish_preflight_check.sh | 4 ++-- .github/workflows/nightly.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- CONTRIBUTING.md | 2 +- prepare_release.sh | 7 +++---- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh index 7c8e384e2..3dda8a367 100755 --- a/.github/scripts/publish_preflight_check.sh +++ b/.github/scripts/publish_preflight_check.sh @@ -122,8 +122,8 @@ echo_info "Generating changelog" echo_info "--------------------------------------------" echo_info "" -echo_info "---< git fetch origin master --prune --unshallow >---" -git fetch origin master --prune --unshallow +echo_info "---< git fetch origin main --prune --unshallow >---" +git fetch origin main --prune --unshallow echo "" echo_info "Generating changelog from history..." diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7bda5ba58..d546b57c5 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -55,7 +55,7 @@ jobs: - name: Send email on failure if: failure() - uses: firebase/firebase-admin-node/.github/actions/send-email@master + uses: firebase/firebase-admin-node/.github/actions/send-email@main with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} @@ -70,7 +70,7 @@ jobs: - name: Send email on cancelled if: cancelled() - uses: firebase/firebase-admin-node/.github/actions/send-email@master + uses: firebase/firebase-admin-node/.github/actions/send-email@main with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86774e50c..f8deca344 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,11 +69,11 @@ jobs: # Check whether the release should be published. We publish only when the trigger PR is # 1. merged - # 2. to the master branch + # 2. to the main branch # 3. with the label 'release:publish', and # 4. the title prefix '[chore] Release '. if: github.event.pull_request.merged && - github.ref == 'refs/heads/master' && + github.ref == 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'release:publish') && startsWith(github.event.pull_request.title, '[chore] Release ') @@ -115,7 +115,7 @@ jobs: - name: Post to Twitter if: success() && contains(github.event.pull_request.labels.*.name, 'release:tweet') - uses: firebase/firebase-admin-node/.github/actions/send-tweet@master + uses: firebase/firebase-admin-node/.github/actions/send-tweet@main with: status: > ${{ steps.preflight.outputs.version }} of @Firebase Admin Java SDK is available. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e6914c5f..907223c5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ Great, we love hearing how we can improve our products! Share you idea through o ## Want to submit a pull request? Sweet, we'd love to accept your contribution! -[Open a new pull request](https://github.com/firebase/firebase-admin-java/pull/new/master) and fill +[Open a new pull request](https://github.com/firebase/firebase-admin-java/pull/new) and fill out the provided template. **If you want to implement a new feature, please open an issue with a proposal first so that we can diff --git a/prepare_release.sh b/prepare_release.sh index b531557ef..16fc646d2 100755 --- a/prepare_release.sh +++ b/prepare_release.sh @@ -66,14 +66,13 @@ if [[ $(git status --porcelain) ]]; then #exit 1 fi -echo "[INFO] Updating the master branch" -git checkout master -git pull origin master +echo "[INFO] Updating the main branch" +git fetch origin main TIMESTAMP=$(date +%s) RELEASE_BRANCH="release-${TIMESTAMP}" echo "[INFO] Creating new release branch: ${RELEASE_BRANCH}" -git checkout -b ${RELEASE_BRANCH} master +git checkout -b ${RELEASE_BRANCH} origin/main ################################# From aa05f35ee91ef6e1d8d75b06d5130a2a57194147 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:53:41 +0000 Subject: [PATCH 173/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.71.0 to 26.72.0 (#1155) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 18b23fd62..99d181c27 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.71.0 + 26.72.0 pom import From bef9fa87666a7dae3bc035a5e72b62f07112a7c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:32:17 -0500 Subject: [PATCH 174/219] chore(deps): Bump io.netty:netty-codec-http (#1157) Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.2.7.Final to 4.2.8.Final. - [Commits](https://github.com/netty/netty/compare/netty-4.2.7.Final...netty-4.2.8.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.8.Final dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 99d181c27..628983544 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.7.Final + 4.2.8.Final From f252bccac4f35abecd646c38d1f43bb2487c1ad3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:35:04 -0500 Subject: [PATCH 175/219] chore(deps): Bump org.apache.maven.plugins:maven-antrun-plugin (#1154) Bumps [org.apache.maven.plugins:maven-antrun-plugin](https://github.com/apache/maven-antrun-plugin) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/apache/maven-antrun-plugin/releases) - [Commits](https://github.com/apache/maven-antrun-plugin/compare/maven-antrun-plugin-3.1.0...maven-antrun-plugin-3.2.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-antrun-plugin dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 628983544..62422b8c9 100644 --- a/pom.xml +++ b/pom.xml @@ -124,7 +124,7 @@ maven-antrun-plugin - 3.1.0 + 3.2.0 site From 653fcb619cfad59b0d5555df95ad8365dd4bb0cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:39:40 -0500 Subject: [PATCH 176/219] chore(deps): Bump org.apache.maven.plugins:maven-source-plugin (#1153) Bumps [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/apache/maven-source-plugin/releases) - [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.1...maven-source-plugin-3.4.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-source-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 62422b8c9..b64053140 100644 --- a/pom.xml +++ b/pom.xml @@ -282,7 +282,7 @@ maven-source-plugin - 3.3.1 + 3.4.0 attach-sources From 0ec2e042151afe75d951022e4ce277d0bfcd67a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:42:05 +0000 Subject: [PATCH 177/219] chore(deps): Bump org.sonatype.central:central-publishing-maven-plugin (#1140) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b64053140..b64ef9af0 100644 --- a/pom.xml +++ b/pom.xml @@ -345,7 +345,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 true central From 88d4b5832b29f773922ea1746847bf6608f490a4 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:40:57 -0500 Subject: [PATCH 178/219] chore: Bump `io.netty` to `4.2.9` (#1159) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b64ef9af0..7a56a39c6 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.8.Final + 4.2.9.Final From 6193e935beae7b74085358dbd7cfb2c0e320a1ab Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:00:23 -0500 Subject: [PATCH 179/219] chore: Update github actions workflows and integration test resources (#1168) * chore: Pinned github actions to a full-length commit SHA * fix: Fixed incorrect hash * chore: Update integration test resources * chore: Added environment label to release action --- .../resources/integ-service-account.json.gpg | Bin 1762 -> 1756 bytes .github/workflows/ci.yml | 4 ++-- .github/workflows/nightly.yml | 10 +++++----- .github/workflows/release.yml | 13 +++++++------ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg index 7740dccd8bdada2eecc181f75c552c00e912e5c2..5a52805c9a854fdea91101dfa1822cdd5e79c874 100644 GIT binary patch literal 1756 zcmV<21|#{54Fm}T3b4{&ennUfga6X%0rw}><=tSBuPhv7FQy8=I?3Lmw%%fwb*z*z z=6KA27D5iuBgx~x$Nyfl&GpjP(Qv$$D`=U2l?uxu35Sn`=|kyz<8?VA+jsyF+7=2@ z<@|}U!==V573LAFo)Fk!&)W@2^oQ_BI8xVK1@Fz@nY52OgWsOxEWs4?ws$bNy2VpekV}20eGu8>0Z((IN{nq@79Jv z>qJM3@B0X3ROSPK`6JTc9rmfZp<%iCo=L%D$)96DgP`ezf<5a55_+{><$|JWn2{6- zu8|~zd^P6mDHo4^^s;zYo=gn$v3_|-6h#Q*v>YB4FwzzYxJ$OFI2qsh-<}WE`UP0o zvNc^F5c1`|u5^Lo6gM(449xUT0J_lPfT`o-Rv3YXMm45zU)xYv%V8E`t!^%3W2T@d zME7SXe{`62EFI}_gg}|*iwSRjWZ)dX-wOf2SENE#%c{=78oej87A$)iafd`bW8SH6 zJ-kB6gL(_ov>A`sM?z99l_M;#(lNwfS+_jn-)(p+mR%!(X&i-aQ;uoOep`8uJh{NU z`;X((7+wEarUlyb_5_u^GXdq zaHU$=fZ8&|fJOjb;?BfCUET*97w=(u`g8iTDN%`zGy@e26Rze|?C=eo-{bX~^~{(OslofH)*vmX`q?uh;mD5YR6@E1U1-|M>X0?* znFf7wQOrblD2(asM#I1dWRAPl@zh(QD z3XsUb(1C36YvZSlmD6!_%N!na+}IMOU|^m>_N`mzbT zZz&Ee7(3&M^=M8Xx_;F26O2}mG$#jR3sA&;ot)ktfCj624V{@9&f|P1`hLMTpnf^v zyhk%W;T7PT7K8gpz*`iIN{Id&dQVMOU|K%KwbY*8!(h}Mh1@VUgaWc{)H1_HNj{KH zq!~m9Ks6xb#27@Jch`+}Rsh!qa(SViGg&w78YzoJc~jys?pKii|FV@TzhUOuk}*;X z4UM8Nu9^|{7&XbCsEJX@rHSaw)%Fuy4-8YCUH<)teY{M^oANVK%EBx4h~)y7RP!u+ zG4q4$%cl%;>0k?v&6UdW=S7~lH-VQ%rOiDIg!tzMdLHwerG6__b|UZ{+x*``6FM_x z9Q%*J0L1Bea||NK+{ND-NlRa5UL%ezMBxFUxC1VlSB|DLWU6iI+7OPXmC?gb}g23VDc?X&{)zPJ~y802E;k33pZuLifi1$Fy+IgceC8QUX(c8vuYUjQKL zPLpo*M3{Hm$;qSSe&l)rR9` zt9n!#Tg#AJ^E292w0{@6@OsPYQ6>R0#1H-qumS64GunHce~@%Y9h397pn53owR5DC zt*{lGzs20XTi%=eqw1~J<2TYgq7Y{bA?-e8DSjCROK>ezwNru{_CUNLu@-pZpX7y~ z;J^s(GiVC9k!jG$m?zZX8w$6!$XoIBdUL|re!W6X_8{PtSQ38mbE@D8(%y$_zcNI6 zQOb{GR=s*=H?m`D`{_yr@g0Jr`3B&P>JtuTtoLNZgc9|vz>at=Wdh*Zk?DLam2X-i zvOz~E+b``WR>({bK{cqmo9hPH%#g(%I1-u!fh6lSWv!AmZ6+;`&jUAf%zUT!Y_d3L ye!cL($Ue&aX9i7=EYc2_I+(Y=Ru;Ur6L%VZx+|5b-zMEMj_Xxk_VrmJgD2Lh0c>#q literal 1762 zcmV<81|9i~4Fm}T0)n*iur~xoxnP%@af)-$RhZ&7)3w(?>+$-b1cw>C)uo%^A>P*=<*0iel03 za}Fsg1`ruYzno_!>aY;)OsTkc%h&&pe{$nZw5?@UjcH^%VjHu)VgwEnKQUK4HJk0= zyZ5gIrQ>xo>l)_!-wlcQad@p8gshGgv1xcg_Yjj!nW#U|n*LMBm3Wlbk?i_CZ5Ume zal+1184AaJ?*V#JRCa=MTz1K6B-nEb*&W)}8_{YN|7UTh+U!ds%WR1v?9Hbxk@ z+EogP!glp>+*yB=n_Y5O!Q;p67Vmj%K0Z&IQHoSQT$5HK`B~z1yiX&eY#WwU=~;7S z*R}F3&c9V&rDM=B?(>!Z9}?HjEfYzeD!pR<0+x@F>TU9zk7}`hv#*;%+wZyrU0HEofh5VjbOiejUW3pY z12D!+Wf#4>(VKPmD}rZ71T zja(Co^VJ`de?Q=DM7vMvigtGHs?54vGS7;%Hh%Kgpcm|%^7?KEi%S69Cr3HH;P{#2 zQKW(H3{(*{l=~Bf7Rl-tT)anXQbTFgdpURPL%zy_*wvVIb#5uC(O*IiG}XiMGUA%y z{hDOjK*5$4CjH6|s5_9ya=cleH7`i?@E_&%aD!Azwfl->_ z*G%8=hHUxvc;-w-8SZhmd?W7x5Q>Gdn&TWCqCer@&YB71xG=dZ z)$Q#4CnyByk9+}lQNhr0X%Yl^Z@x^E{?CS9l0qFDy5|%MRx3RMp&FRUbXi#z!svt^ z$1+LBRDKak{r8lR!uG7M$3y`i=mM%I6C;mOE*azp=yC&EjEO@hZAV~%vb<>vw*Tx}<@1-F%E1MJ z&Kx%>OC=ss;A2c#Xs*e!O^%Y!Dj=VPAc>{~z^9u8{@H%2A-4|8w>siL6sFHXXGAGp z6u#;-ci&~<6YWll-R0BZ(9=FJ*d=Z^1zpP&loXySLaK2LILSvW!Dc?^#B&6$r*#Z0VQL=xlo$_b_5MO*>kW6!}M4b=rO4;kC? z7>1#k9x6oenqHJ=>|?M)^;{lP0reAV^DsBc!yt{hJ=)44ze!{NMcolK=)E*z3xPIFQ}^9xIoZ8E2fWR@#G|3UOCwegz@y^#$8?WWnJ zs2>$dD-U!>e@QG8RfQO0o(=0E;V|(Wz7kVeam0oZT7r|6xiFJ zNd8hqdzqJ0-`fL-a@okskU*`zWMi=%n1PPxq!&m!cL|J#l~QZ}A3JebK?T zy$sIEE!|IkVGk9;6HzHA)9Y-IXTCA)$QGOVqdT1OhM%`yYT53qvYtc=4KjLTfqCH9 zgC*H0Jk^%P@UH!391-ApSu$Z{4D#3;W{vq*kO zsL+qpghA`Jm|4{gJrTOb%(>>3vR5{Am*n3SBI9H-+h-WW$}kgTg9MyYUUi*K+#nml Ero6&wRR910 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39948e9e9..c26cf2282 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,10 @@ jobs: java-version: [8, 11, 17] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 with: distribution: 'zulu' java-version: ${{ matrix.java-version }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d546b57c5..f7a9d9894 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -29,12 +29,12 @@ jobs: steps: - name: Checkout source for staging - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.event.client_payload.ref || github.ref }} - name: Set up JDK 8 - uses: actions/setup-java@v4 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 with: distribution: 'zulu' java-version: 8 @@ -48,14 +48,14 @@ jobs: # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. - name: Archive artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dist path: dist - name: Send email on failure if: failure() - uses: firebase/firebase-admin-node/.github/actions/send-email@main + uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} @@ -70,7 +70,7 @@ jobs: - name: Send email on cancelled if: cancelled() - uses: firebase/firebase-admin-node/.github/actions/send-email@main + uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8deca344..d261f4757 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,12 +40,12 @@ jobs: # via the 'ref' client parameter. steps: - name: Checkout source for staging - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.event.client_payload.ref || github.ref }} - name: Set up JDK 8 - uses: actions/setup-java@v4 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 with: distribution: 'zulu' java-version: 8 @@ -59,7 +59,7 @@ jobs: # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. - name: Archive artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dist path: dist @@ -78,15 +78,16 @@ jobs: startsWith(github.event.pull_request.title, '[chore] Release ') runs-on: ubuntu-latest + environment: Release permissions: contents: write steps: - name: Checkout source for publish - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up JDK 8 - uses: actions/setup-java@v4 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 with: distribution: 'zulu' java-version: 8 @@ -115,7 +116,7 @@ jobs: - name: Post to Twitter if: success() && contains(github.event.pull_request.labels.*.name, 'release:tweet') - uses: firebase/firebase-admin-node/.github/actions/send-tweet@main + uses: firebase/firebase-admin-node/.github/actions/send-tweet@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 with: status: > ${{ steps.preflight.outputs.version }} of @Firebase Admin Java SDK is available. From a0450a57fa31da339e7b247432e56d372b418313 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:12:13 -0500 Subject: [PATCH 180/219] chore: Update Github action resources (#1172) * chore: Update Github action resources * chore: Set public GPG key id as a environment variable * chore: Change env type --- .github/resources/firebase.asc.gpg | Bin 4078 -> 787 bytes .github/resources/settings.xml | 2 +- .github/workflows/release.yml | 15 +-------------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg index feb690edb55d475e520c91829b1bddf74e5210a3..4b41f18c5676a2a784d2b9eb6a7440a1e19be399 100644 GIT binary patch literal 787 zcmV+u1MK{a4Fm}T3KBnfhC?eYL;uq00cSetCi>1qlz^Xi-ozFcZ581Y5}hR{Vn#>= z=D#J*?YNUq1sj|JNVWU*e?GMSgamDdJ_yKf37G4v!pE!mzjXdfM;5IicmM)~pWQ7h zjc*N~XZ7f40y8N5!#DvLy?F5=ou$AY6O?)f0A3AXh1^6KDm3EX8OZ?iISX;&?(p}% zu{acj-|7myRgG@J*@J7!JJr6A^%-rJl(A;rKnR#u=bla0aFTL!X?o>&8n)c^+sQee zJMim5j&N{-W>GL?T0hFK$$5mPR%t+*6_{LIlE zev2=gs1Pc6JjuciM3){bDbH*)9QCL$nnI%VDV+&J`3#IM|9_u~M@>t(PWmvswMWPJ zND8@u^;z=?R8W{xBTUDkn_36+NU4iDckJ-Q2^f>CH$|M8-EsW3jD-4}x5?Q$*Z#l8 z1NrSQ#?aBMYTJW1?kY(`HhCuMljp>a1O&GGC(6IQ|KXmkL5tU`c-K@v%J5(}p%YN$ zMu)MtU`cX7nl;%OtU-FxmPp1Lflf&j;=O^7zI)Vf!K*%s#$6z(DF(CL~9M`Kjj|M}HG0rv989>CiWe^MG$^Wo6pSKDZOM zEh;%$4+zCii~(zf`W@~$BTwe3`!Q*wLbi@Z5dO~{z&(q7Xv~MuN$HRjTp^>vtVU-B zQt~VZd-uiftj{6?H;7I@boJAswpY3nwkHB;X$!kAAJN`k@9oF*YW&6UTbizb9A0Do zDSBB2dgi{Op-O5|X)VyL2`MN!Z@>c+cp^8`@liOjtg;4+i literal 4078 zcmV@KM2ENfd` znc#W*a3Ooqd*(W{g1h_a8@BIbn7Nt>^*8M7f?&enK2bc&q|G~nl#e|f<2iTRItr2~ z<8LWj4`IS0eBc)Y)1(D8Qnh*R5a*?g zt;VUB`_4$)G30ADeXbW9j_^MOT>5b6LVbl;EQ6{8%}k`3TzszD*fk!l?!-5&Twqgm ziC_HBh84?sC2A)O@^nnt7X)m6H-#dVQm$bp#vd~TNg@pm;`L=y4cb^-cIFte9V3e?Y2`s|I&Xw-=Xhff??~-R>{K%)84+;-8g|Ju0*l&04TLb4b-vPvL8lrFk~VW zs>D<1-tYm>mI9$KEo#j_i9{%EOqaq^H6i(>GU#}5aAEh`B8A*_F%EbEbVld04aA4g z6}J6Ek+CfOJc!6N;d;^6(~Q~-jIsN>3#ER;5E{fK9v}qQqf- zoJ!LThr^Y{843w;o|SUab2q8Hbi9WC6_1)$xJW4Mhq%YOwSW4=6q9ASm=7VAzhcr3 z1w)OTjuA;1PaWv`FdrOZ^Hs-28s_c`@ua&Ah$ zi${nW!Vk$Z7x<4l(!HbY6)KyS|gW`KGoitXa8m%#-k>|5@Y8mkoC{c8~TKdIRL7r(AdMf_+~wYXRQxht!( zD{D zL{JkyB6SRl*~G5Ty$PDu6TG7?hqbUDx3$d4=#98gb&7iabITcOFE=}Ss#bill^)Tj z+gR2=zxAs9GAh)Ql~}yg+YX=={Wil5%>gic6!6^bRzp#?K9nZWDI$JE(>Lz$YQZb; z^v`lltn%;Sj5-aNQG;43ujMC&T2;yThD&)`FiiQpV}|2(>GNXh2d*Cx1t#F$h=2pSCGfKlunKi$YK57L56Dtr=fRRtj-qI2kNpi}2Q^ zB154`675|)zdKJy;EBb+_Jo$RnU3UjD^3;H=Ethqt$Y6s8kpNW>j@>XT#InnAQ#Tv zq{3?io7fp+&A;QxCS}u~JcS*SZJt8Dmf#lj3s{rUBnkE?RwO5YuyG{{)3;Ij? zFHspS+eq|NtP3W8jn?`e`&!|h@uZ6m?D>6?9aYg0R+(~i0SjaI5?+{kOZKNHs8-*X z*@;2nopIv7&asv{fQEG!01RN5w!HI@g;u=C{_hsZ!Cldo%6Tl>LNkfbCu;!ozTH4L zXyu~Ea2~aULuul^v^0j*&VQ>0ApoG+Ocuu#P@&2NHKYdAP5SEomQ)(H7+x_2VRU8+ zjtU`Zra4$+*g$n@{8M$Mf@gk_545HMU-o`~-JF6SiL02m zd)2{k)qETfW-|8%feRGm6eo+(qV?=e5sqXkqnS}W+8>bmM0I#dp!PbK#rXX;r!Fop z?osyTL~1kCR1A<L_FImJFXKI zD}Izrm6!-7v8V*|x2dob34a`$X$du_B@WeeA&$47OfPB{*R*avqx9|adC~JVrbbcX zKQ<~HEIAAGd8AS4op4DO40s4*}tFidP8!l+~>c%#?t$br$ zlUw(xg?Vyq>|i{tc7|i%s8PXDeq}dJ+X^Hs+fCg-0Sq25_!!3{H5vn3Y~c04T@-Wm z{2;spuy=7kYxC&EL~IW0C$y&ViMQM7Hib|o@&=kyVDcjs72ZW9u#vR8jglzn!Tr zUVdnmKBskyd1hDdMbOWcZQ1;7+{xJD_&6c*&nZ9#m2!GX<(-^`<-p5gc`n35Trm=R z0qXbS;F4TWjruUJc`>la!PE0h=05=4=zY+lxer^l)aPkzF+cYyOo!(Zd+-s^=5?kJ zbmUI|xUdW&`s$(lVW7qSs*YT_X5Xgl2KzN)ijnrEIHPvO`@;>hYGRF>X6V(71P9eh#z8xW&N6 z&D_j?e_LysY$B}2!}8{m2j%>aC}mK{^80`_P-I<>gu{y&JlrG?SQI3d=_H_#9l`yk zSr6;J9t#BIcWMC|o{9b(=D*O+kkg-%DbT|s+(<1ldE+LsMW8q>)k8&BpANs1gdeV5 zvd;hi+(&s`BQ?iu$n{MkP?_$1?QsT~`mpl^u^$sOe5MoctjNY6MYsqIYLQeR%PG+u zzZ<_}b%Tb>WP&oC@H<+SL-&_}^x>A{%gGsdWHE8vZW6O+J69RRC97bPiuO{_1av>+ zxy8JkX!S%-8x^?9T0VE8u9vDo4b{h&%0pVHF(+6I1g2AdU|uB{Vhry}j0S3#s>mo2 zbh>&S!<2$uiD*{;aHw<}=|%Ht3vi+v0521EGkJhw>ow?sIt3jPO86zHt$p~WJbt10 ziM#lf{DnH$!5SAfC?0amV>nBO;zgE@(xA7ZPKbDB`7F+wL(Nj|ubUx8f<|MV^!*l< zZ465}A%c3VXdFyL1`DCwHUl6djAmD~0_D_Y8Li!ZD&C7!TlE22?>X|Z>9V?dz;q%L z4jwEMo_GHOrVAex0Dool7CwI2rSP$-K6m->U z5$TGTK0S2_9Rq6q^_t`poDSrkUzMvvpv|@|7uX!C7W)k{^k?Rd8TPl}oXf>?RB=MV zC-bGECQ_xMxCEyxTsNWypXXwvmQbWWMC?e8x9J<^hxK>%B@kSBjOEPsPSx2s$&jzw zUmAwjBVECS8p`cl&~ch1-kD+;A+-N8r2T{t%oXns?OaCP{4a^RlyKB~XR_yGXST0r z+q^%bJS0W)C<6XAo{P3XtS_OnvUiOya6#tOEg6Ok^uP?r5~%VjI{WcJ)c-bqJ1M`4VkU*Z&S=hs13i$!t8MK zxhR1Or*Dc+w1Mxk6A$X$zk&--%wXXj4ZvTQ<+qF{a~Az9#-IRyw+bFeuMOqqyzNb9e0jgD9K%dSDj*zb?{vRYnL|$) zSF?pTBkqMIYho$kKK!mAN&nEI_gTf7hO|cEnq>#KsO?p%t3X{D((akW-ghN2JhtMQ z3R-6vnBTd&=Ax8h-S(cCrqVb?)kkZ_Nl?l6JyEW zDuxY(4xmK}UEs=<^4pp29v`<&_Q4 zvr9p-TjdRBaUj(A=EV3D7%WS$HU7Wl?$pAz4J{n7oqET#Wj!ad$s)GxAm6lwu&m@jqXF2t7fY&)M0JbQAeBLHp=odW_)Iz3(<{dUbrYik5#>A^QO zj~xc02=e9?)Z~_2t?!_k`(ZmsX+Q6fmE}U@afYv(ORqUl06go~;U`0Kz+w$1Q(;!` zXh^_0zX&PO9lXa}GeDj>x|eC`K`&7H=qWI!sxLC5j!_5c6? diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml index da1d3057d..53e482738 100644 --- a/.github/resources/settings.xml +++ b/.github/resources/settings.xml @@ -21,7 +21,7 @@ gpg - 0A05D8FAD4287A36C53BE07714D6B82AEB1DD39C + ${env.GPG_KEY_ID} ${env.GPG_PASSPHRASE} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d261f4757..0a4c91f91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,6 +101,7 @@ jobs: env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }} @@ -112,17 +113,3 @@ jobs: --title "Firebase Admin Java SDK ${{ steps.preflight.outputs.version }}" --notes '${{ steps.preflight.outputs.changelog }}' - # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - - name: Post to Twitter - if: success() && - contains(github.event.pull_request.labels.*.name, 'release:tweet') - uses: firebase/firebase-admin-node/.github/actions/send-tweet@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 - with: - status: > - ${{ steps.preflight.outputs.version }} of @Firebase Admin Java SDK is available. - https://github.com/firebase/firebase-admin-java/releases/tag/${{ steps.preflight.outputs.version }} - consumer-key: ${{ secrets.TWITTER_CONSUMER_KEY }} - consumer-secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} - access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} - access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - continue-on-error: true From eaa32258b9fc75634c7ab3762488592f37128aef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:23:51 -0500 Subject: [PATCH 181/219] chore(deps): Bump org.sonatype.central:central-publishing-maven-plugin (#1169) Bumps [org.sonatype.central:central-publishing-maven-plugin](https://github.com/sonatype/central-publishing-maven-plugin) from 0.9.0 to 0.10.0. - [Commits](https://github.com/sonatype/central-publishing-maven-plugin/commits) --- updated-dependencies: - dependency-name: org.sonatype.central:central-publishing-maven-plugin dependency-version: 0.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7a56a39c6..8f1ec9d4a 100644 --- a/pom.xml +++ b/pom.xml @@ -345,7 +345,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.9.0 + 0.10.0 true central From ad2ff3ed49985ed418dbaa28a0b82000cac02ab8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:33:20 -0500 Subject: [PATCH 182/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.72.0 to 26.73.0 (#1163) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.72.0 to 26.73.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.72.0...v26.73.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.73.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8f1ec9d4a..4a7aad760 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.72.0 + 26.74.0 pom import From 27ec3f9702ce476449d70e53ad9cfd2157d8e6c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:41:10 -0500 Subject: [PATCH 183/219] chore(deps): Bump org.codehaus.mojo:exec-maven-plugin (#1165) Bumps [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) from 3.6.2 to 3.6.3. - [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases) - [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.6.2...3.6.3) --- updated-dependencies: - dependency-name: org.codehaus.mojo:exec-maven-plugin dependency-version: 3.6.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4a7aad760..df3d4ae99 100644 --- a/pom.xml +++ b/pom.xml @@ -208,7 +208,7 @@ org.codehaus.mojo exec-maven-plugin - 3.6.2 + 3.6.3 test From eea335afdc882c76bb3309805b98275e6dfd822b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 28 Jan 2026 12:50:46 -0500 Subject: [PATCH 184/219] chore: Update copyright headers to Google LLC (#1132) * style: Update copyright headers to Google LLC * Added non src files --- .github/scripts/generate_changelog.sh | 2 +- .github/scripts/package_artifacts.sh | 2 +- .github/scripts/publish_artifacts.sh | 2 +- .github/scripts/publish_preflight_check.sh | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- NOTICE.txt | 2 +- checkstyle.xml | 2 +- pom.xml | 2 +- prepare_release.sh | 2 +- src/main/java/com/google/firebase/ErrorCode.java | 2 +- src/main/java/com/google/firebase/FirebaseApp.java | 2 +- .../java/com/google/firebase/FirebaseAppLifecycleListener.java | 2 +- src/main/java/com/google/firebase/FirebaseException.java | 2 +- src/main/java/com/google/firebase/FirebaseOptions.java | 2 +- src/main/java/com/google/firebase/ImplFirebaseTrampolines.java | 2 +- src/main/java/com/google/firebase/IncomingHttpResponse.java | 2 +- src/main/java/com/google/firebase/OutgoingHttpRequest.java | 2 +- src/main/java/com/google/firebase/ThreadManager.java | 2 +- src/main/java/com/google/firebase/auth/ActionCodeSettings.java | 2 +- src/main/java/com/google/firebase/auth/AuthErrorCode.java | 2 +- src/main/java/com/google/firebase/auth/DeleteUsersResult.java | 2 +- src/main/java/com/google/firebase/auth/EmailIdentifier.java | 2 +- src/main/java/com/google/firebase/auth/ErrorInfo.java | 2 +- src/main/java/com/google/firebase/auth/ExportedUserRecord.java | 2 +- .../java/com/google/firebase/auth/FirebaseAuthException.java | 2 +- src/main/java/com/google/firebase/auth/FirebaseToken.java | 2 +- src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java | 2 +- .../java/com/google/firebase/auth/FirebaseTokenVerifier.java | 2 +- .../com/google/firebase/auth/FirebaseTokenVerifierImpl.java | 2 +- src/main/java/com/google/firebase/auth/FirebaseUserManager.java | 2 +- src/main/java/com/google/firebase/auth/GetUsersResult.java | 2 +- src/main/java/com/google/firebase/auth/ImportUserRecord.java | 2 +- src/main/java/com/google/firebase/auth/ListUsersPage.java | 2 +- src/main/java/com/google/firebase/auth/PhoneIdentifier.java | 2 +- src/main/java/com/google/firebase/auth/ProviderIdentifier.java | 2 +- src/main/java/com/google/firebase/auth/ProviderUserInfo.java | 2 +- .../java/com/google/firebase/auth/RevocationCheckDecorator.java | 2 +- .../java/com/google/firebase/auth/SessionCookieOptions.java | 2 +- src/main/java/com/google/firebase/auth/UidIdentifier.java | 2 +- src/main/java/com/google/firebase/auth/UserIdentifier.java | 2 +- src/main/java/com/google/firebase/auth/UserImportHash.java | 2 +- src/main/java/com/google/firebase/auth/UserImportOptions.java | 2 +- src/main/java/com/google/firebase/auth/UserImportResult.java | 2 +- src/main/java/com/google/firebase/auth/UserInfo.java | 2 +- src/main/java/com/google/firebase/auth/UserMetadata.java | 2 +- src/main/java/com/google/firebase/auth/UserProvider.java | 2 +- src/main/java/com/google/firebase/auth/UserRecord.java | 2 +- src/main/java/com/google/firebase/auth/hash/Argon2.java | 2 +- src/main/java/com/google/firebase/auth/hash/Bcrypt.java | 2 +- src/main/java/com/google/firebase/auth/hash/Hmac.java | 2 +- src/main/java/com/google/firebase/auth/hash/HmacMd5.java | 2 +- src/main/java/com/google/firebase/auth/hash/HmacSha1.java | 2 +- src/main/java/com/google/firebase/auth/hash/HmacSha256.java | 2 +- src/main/java/com/google/firebase/auth/hash/HmacSha512.java | 2 +- src/main/java/com/google/firebase/auth/hash/Md5.java | 2 +- src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java | 2 +- src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java | 2 +- src/main/java/com/google/firebase/auth/hash/RepeatableHash.java | 2 +- src/main/java/com/google/firebase/auth/hash/Scrypt.java | 2 +- src/main/java/com/google/firebase/auth/hash/Sha1.java | 2 +- src/main/java/com/google/firebase/auth/hash/Sha256.java | 2 +- src/main/java/com/google/firebase/auth/hash/Sha512.java | 2 +- src/main/java/com/google/firebase/auth/hash/StandardScrypt.java | 2 +- .../com/google/firebase/auth/internal/AuthErrorHandler.java | 2 +- .../java/com/google/firebase/auth/internal/AuthHttpClient.java | 2 +- .../com/google/firebase/auth/internal/BatchDeleteResponse.java | 2 +- .../java/com/google/firebase/auth/internal/CryptoSigner.java | 2 +- .../google/firebase/auth/internal/DownloadAccountResponse.java | 2 +- .../google/firebase/auth/internal/FirebaseCustomAuthToken.java | 2 +- .../com/google/firebase/auth/internal/FirebaseTokenFactory.java | 2 +- .../google/firebase/auth/internal/GetAccountInfoRequest.java | 2 +- .../google/firebase/auth/internal/GetAccountInfoResponse.java | 2 +- .../google/firebase/auth/internal/UploadAccountResponse.java | 2 +- src/main/java/com/google/firebase/auth/internal/Utils.java | 2 +- .../java/com/google/firebase/auth/internal/package-info.java | 2 +- .../google/firebase/auth/multitenancy/FirebaseTenantClient.java | 2 +- src/main/java/com/google/firebase/cloud/StorageClient.java | 2 +- .../java/com/google/firebase/database/ChildEventListener.java | 2 +- src/main/java/com/google/firebase/database/DataSnapshot.java | 2 +- src/main/java/com/google/firebase/database/DatabaseError.java | 2 +- .../java/com/google/firebase/database/DatabaseException.java | 2 +- .../java/com/google/firebase/database/DatabaseReference.java | 2 +- src/main/java/com/google/firebase/database/Exclude.java | 2 +- .../java/com/google/firebase/database/FirebaseDatabase.java | 2 +- .../java/com/google/firebase/database/GenericTypeIndicator.java | 2 +- .../com/google/firebase/database/IgnoreExtraProperties.java | 2 +- src/main/java/com/google/firebase/database/InternalHelpers.java | 2 +- src/main/java/com/google/firebase/database/MutableData.java | 2 +- src/main/java/com/google/firebase/database/OnDisconnect.java | 2 +- src/main/java/com/google/firebase/database/PropertyName.java | 2 +- src/main/java/com/google/firebase/database/Query.java | 2 +- src/main/java/com/google/firebase/database/ServerValue.java | 2 +- .../com/google/firebase/database/ThrowOnExtraProperties.java | 2 +- src/main/java/com/google/firebase/database/Transaction.java | 2 +- .../java/com/google/firebase/database/ValueEventListener.java | 2 +- .../java/com/google/firebase/database/annotations/NotNull.java | 2 +- .../java/com/google/firebase/database/annotations/Nullable.java | 2 +- .../com/google/firebase/database/annotations/package-info.java | 2 +- .../com/google/firebase/database/collection/ArraySortedMap.java | 2 +- .../google/firebase/database/collection/ImmutableSortedMap.java | 2 +- .../database/collection/ImmutableSortedMapIterator.java | 2 +- .../google/firebase/database/collection/ImmutableSortedSet.java | 2 +- .../google/firebase/database/collection/LLRBBlackValueNode.java | 2 +- .../com/google/firebase/database/collection/LLRBEmptyNode.java | 2 +- .../java/com/google/firebase/database/collection/LLRBNode.java | 2 +- .../google/firebase/database/collection/LLRBRedValueNode.java | 2 +- .../com/google/firebase/database/collection/LLRBValueNode.java | 2 +- .../google/firebase/database/collection/RBTreeSortedMap.java | 2 +- .../google/firebase/database/collection/StandardComparator.java | 2 +- .../com/google/firebase/database/collection/package-info.java | 2 +- .../com/google/firebase/database/connection/CompoundHash.java | 2 +- .../com/google/firebase/database/connection/Connection.java | 2 +- .../database/connection/ConnectionAuthTokenProvider.java | 2 +- .../google/firebase/database/connection/ConnectionContext.java | 2 +- .../google/firebase/database/connection/ConnectionUtils.java | 2 +- .../java/com/google/firebase/database/connection/Constants.java | 2 +- .../java/com/google/firebase/database/connection/HostInfo.java | 2 +- .../google/firebase/database/connection/ListenHashProvider.java | 2 +- .../firebase/database/connection/PersistentConnection.java | 2 +- .../firebase/database/connection/PersistentConnectionImpl.java | 2 +- .../com/google/firebase/database/connection/RangeMerge.java | 2 +- .../firebase/database/connection/RequestResultCallback.java | 2 +- .../firebase/database/connection/WebsocketConnection.java | 2 +- .../com/google/firebase/database/connection/package-info.java | 2 +- .../google/firebase/database/connection/util/RetryHelper.java | 2 +- .../google/firebase/database/connection/util/package-info.java | 2 +- .../com/google/firebase/database/core/AuthTokenProvider.java | 2 +- .../google/firebase/database/core/ChildEventRegistration.java | 2 +- .../java/com/google/firebase/database/core/CompoundWrite.java | 2 +- src/main/java/com/google/firebase/database/core/Constants.java | 2 +- src/main/java/com/google/firebase/database/core/Context.java | 2 +- .../java/com/google/firebase/database/core/DatabaseConfig.java | 2 +- .../com/google/firebase/database/core/EventRegistration.java | 2 +- .../firebase/database/core/EventRegistrationZombieListener.java | 2 +- .../java/com/google/firebase/database/core/EventTarget.java | 2 +- .../com/google/firebase/database/core/JvmAuthTokenProvider.java | 2 +- .../java/com/google/firebase/database/core/JvmPlatform.java | 2 +- src/main/java/com/google/firebase/database/core/Path.java | 2 +- src/main/java/com/google/firebase/database/core/Platform.java | 2 +- src/main/java/com/google/firebase/database/core/Repo.java | 2 +- src/main/java/com/google/firebase/database/core/RepoInfo.java | 2 +- .../java/com/google/firebase/database/core/RepoManager.java | 2 +- src/main/java/com/google/firebase/database/core/RunLoop.java | 2 +- .../java/com/google/firebase/database/core/ServerValues.java | 2 +- .../java/com/google/firebase/database/core/SnapshotHolder.java | 2 +- .../com/google/firebase/database/core/SparseSnapshotTree.java | 2 +- src/main/java/com/google/firebase/database/core/SyncPoint.java | 2 +- src/main/java/com/google/firebase/database/core/SyncTree.java | 2 +- src/main/java/com/google/firebase/database/core/Tag.java | 2 +- .../google/firebase/database/core/ThreadPoolEventTarget.java | 2 +- .../java/com/google/firebase/database/core/UserWriteRecord.java | 2 +- .../java/com/google/firebase/database/core/ValidationPath.java | 2 +- .../google/firebase/database/core/ValueEventRegistration.java | 2 +- src/main/java/com/google/firebase/database/core/WriteTree.java | 2 +- .../java/com/google/firebase/database/core/WriteTreeRef.java | 2 +- .../com/google/firebase/database/core/ZombieEventManager.java | 2 +- .../google/firebase/database/core/operation/AckUserWrite.java | 2 +- .../google/firebase/database/core/operation/ListenComplete.java | 2 +- .../java/com/google/firebase/database/core/operation/Merge.java | 2 +- .../com/google/firebase/database/core/operation/Operation.java | 2 +- .../firebase/database/core/operation/OperationSource.java | 2 +- .../com/google/firebase/database/core/operation/Overwrite.java | 2 +- .../google/firebase/database/core/operation/package-info.java | 2 +- .../java/com/google/firebase/database/core/package-info.java | 2 +- .../google/firebase/database/core/persistence/CachePolicy.java | 2 +- .../database/core/persistence/DefaultPersistenceManager.java | 2 +- .../firebase/database/core/persistence/LRUCachePolicy.java | 2 +- .../database/core/persistence/NoopPersistenceManager.java | 2 +- .../firebase/database/core/persistence/PersistenceManager.java | 2 +- .../database/core/persistence/PersistenceStorageEngine.java | 2 +- .../google/firebase/database/core/persistence/PruneForest.java | 2 +- .../google/firebase/database/core/persistence/TrackedQuery.java | 2 +- .../firebase/database/core/persistence/TrackedQueryManager.java | 2 +- .../google/firebase/database/core/persistence/package-info.java | 2 +- .../google/firebase/database/core/utilities/ImmutableTree.java | 2 +- .../com/google/firebase/database/core/utilities/Predicate.java | 2 +- .../java/com/google/firebase/database/core/utilities/Tree.java | 2 +- .../com/google/firebase/database/core/utilities/TreeNode.java | 2 +- .../google/firebase/database/core/utilities/package-info.java | 2 +- .../java/com/google/firebase/database/core/view/CacheNode.java | 2 +- .../com/google/firebase/database/core/view/CancelEvent.java | 2 +- .../java/com/google/firebase/database/core/view/Change.java | 2 +- .../java/com/google/firebase/database/core/view/DataEvent.java | 2 +- src/main/java/com/google/firebase/database/core/view/Event.java | 2 +- .../com/google/firebase/database/core/view/EventGenerator.java | 2 +- .../com/google/firebase/database/core/view/EventRaiser.java | 2 +- .../com/google/firebase/database/core/view/QueryParams.java | 2 +- .../java/com/google/firebase/database/core/view/QuerySpec.java | 2 +- src/main/java/com/google/firebase/database/core/view/View.java | 2 +- .../java/com/google/firebase/database/core/view/ViewCache.java | 2 +- .../com/google/firebase/database/core/view/ViewProcessor.java | 2 +- .../database/core/view/filter/ChildChangeAccumulator.java | 2 +- .../firebase/database/core/view/filter/IndexedFilter.java | 2 +- .../firebase/database/core/view/filter/LimitedFilter.java | 2 +- .../google/firebase/database/core/view/filter/NodeFilter.java | 2 +- .../google/firebase/database/core/view/filter/RangedFilter.java | 2 +- .../google/firebase/database/core/view/filter/package-info.java | 2 +- .../com/google/firebase/database/core/view/package-info.java | 2 +- .../java/com/google/firebase/database/snapshot/BooleanNode.java | 2 +- .../java/com/google/firebase/database/snapshot/ChildKey.java | 2 +- .../com/google/firebase/database/snapshot/ChildrenNode.java | 2 +- .../com/google/firebase/database/snapshot/CompoundHash.java | 2 +- .../google/firebase/database/snapshot/DeferredValueNode.java | 2 +- .../java/com/google/firebase/database/snapshot/DoubleNode.java | 2 +- .../java/com/google/firebase/database/snapshot/EmptyNode.java | 2 +- src/main/java/com/google/firebase/database/snapshot/Index.java | 2 +- .../java/com/google/firebase/database/snapshot/IndexedNode.java | 2 +- .../java/com/google/firebase/database/snapshot/KeyIndex.java | 2 +- .../java/com/google/firebase/database/snapshot/LeafNode.java | 2 +- .../java/com/google/firebase/database/snapshot/LongNode.java | 2 +- .../java/com/google/firebase/database/snapshot/NamedNode.java | 2 +- src/main/java/com/google/firebase/database/snapshot/Node.java | 2 +- .../com/google/firebase/database/snapshot/NodeUtilities.java | 2 +- .../java/com/google/firebase/database/snapshot/PathIndex.java | 2 +- .../com/google/firebase/database/snapshot/PriorityIndex.java | 2 +- .../google/firebase/database/snapshot/PriorityUtilities.java | 2 +- .../java/com/google/firebase/database/snapshot/RangeMerge.java | 2 +- .../java/com/google/firebase/database/snapshot/StringNode.java | 2 +- .../java/com/google/firebase/database/snapshot/ValueIndex.java | 2 +- .../com/google/firebase/database/snapshot/package-info.java | 2 +- .../java/com/google/firebase/database/util/EmulatorHelper.java | 2 +- src/main/java/com/google/firebase/database/util/GAuthToken.java | 2 +- src/main/java/com/google/firebase/database/util/JsonMapper.java | 2 +- .../java/com/google/firebase/database/util/package-info.java | 2 +- src/main/java/com/google/firebase/database/utilities/Clock.java | 2 +- .../com/google/firebase/database/utilities/DefaultClock.java | 2 +- .../com/google/firebase/database/utilities/DefaultRunLoop.java | 2 +- .../google/firebase/database/utilities/NodeSizeEstimator.java | 2 +- .../com/google/firebase/database/utilities/OffsetClock.java | 2 +- src/main/java/com/google/firebase/database/utilities/Pair.java | 2 +- .../java/com/google/firebase/database/utilities/ParsedUrl.java | 2 +- .../com/google/firebase/database/utilities/PushIdGenerator.java | 2 +- .../java/com/google/firebase/database/utilities/Utilities.java | 2 +- .../java/com/google/firebase/database/utilities/Validation.java | 2 +- .../firebase/database/utilities/encoding/CustomClassMapper.java | 2 +- .../firebase/database/utilities/encoding/package-info.java | 2 +- .../com/google/firebase/database/utilities/package-info.java | 2 +- src/main/java/com/google/firebase/iid/FirebaseInstanceId.java | 2 +- .../com/google/firebase/iid/FirebaseInstanceIdException.java | 2 +- .../com/google/firebase/internal/AbstractHttpErrorHandler.java | 2 +- .../google/firebase/internal/AbstractPlatformErrorHandler.java | 2 +- .../firebase/internal/ApacheHttp2AsyncEntityProducer.java | 2 +- .../java/com/google/firebase/internal/ApacheHttp2Request.java | 2 +- .../java/com/google/firebase/internal/ApacheHttp2Response.java | 2 +- .../java/com/google/firebase/internal/ApacheHttp2Transport.java | 2 +- src/main/java/com/google/firebase/internal/ApiClientUtils.java | 2 +- .../internal/ApplicationDefaultCredentialsProvider.java | 2 +- .../java/com/google/firebase/internal/CallableOperation.java | 2 +- .../java/com/google/firebase/internal/EmulatorCredentials.java | 2 +- .../com/google/firebase/internal/ErrorHandlingHttpClient.java | 2 +- .../google/firebase/internal/FirebaseProcessEnvironment.java | 2 +- .../google/firebase/internal/FirebaseRequestInitializer.java | 2 +- .../com/google/firebase/internal/FirebaseScheduledExecutor.java | 2 +- src/main/java/com/google/firebase/internal/FirebaseService.java | 2 +- .../com/google/firebase/internal/FirebaseThreadManagers.java | 2 +- src/main/java/com/google/firebase/internal/GuardedBy.java | 2 +- .../java/com/google/firebase/internal/HttpErrorHandler.java | 2 +- src/main/java/com/google/firebase/internal/HttpRequestInfo.java | 2 +- .../google/firebase/internal/ListenableFuture2ApiFuture.java | 2 +- .../google/firebase/internal/MockApacheHttp2AsyncClient.java | 2 +- src/main/java/com/google/firebase/internal/NonNull.java | 2 +- src/main/java/com/google/firebase/internal/Nullable.java | 2 +- src/main/java/com/google/firebase/internal/RetryConfig.java | 2 +- .../java/com/google/firebase/internal/RetryInitializer.java | 2 +- .../firebase/internal/RetryUnsuccessfulResponseHandler.java | 2 +- src/main/java/com/google/firebase/internal/SdkUtils.java | 2 +- src/main/java/com/google/firebase/internal/package-info.java | 2 +- src/main/java/com/google/firebase/messaging/AndroidConfig.java | 2 +- .../java/com/google/firebase/messaging/AndroidNotification.java | 2 +- src/main/java/com/google/firebase/messaging/ApnsConfig.java | 2 +- src/main/java/com/google/firebase/messaging/Aps.java | 2 +- src/main/java/com/google/firebase/messaging/ApsAlert.java | 2 +- src/main/java/com/google/firebase/messaging/BatchResponse.java | 2 +- .../java/com/google/firebase/messaging/BatchResponseImpl.java | 2 +- src/main/java/com/google/firebase/messaging/CriticalSound.java | 2 +- .../java/com/google/firebase/messaging/FirebaseMessaging.java | 2 +- .../google/firebase/messaging/FirebaseMessagingClientImpl.java | 2 +- .../google/firebase/messaging/FirebaseMessagingException.java | 2 +- .../com/google/firebase/messaging/InstanceIdClientImpl.java | 2 +- src/main/java/com/google/firebase/messaging/LightSettings.java | 2 +- .../java/com/google/firebase/messaging/LightSettingsColor.java | 2 +- src/main/java/com/google/firebase/messaging/Message.java | 2 +- .../java/com/google/firebase/messaging/MulticastMessage.java | 2 +- src/main/java/com/google/firebase/messaging/Notification.java | 2 +- src/main/java/com/google/firebase/messaging/SendResponse.java | 2 +- .../com/google/firebase/messaging/TopicManagementResponse.java | 2 +- src/main/java/com/google/firebase/messaging/WebpushConfig.java | 2 +- .../java/com/google/firebase/messaging/WebpushNotification.java | 2 +- .../com/google/firebase/messaging/internal/package-info.java | 2 +- .../java/com/google/firebase/projectmanagement/AndroidApp.java | 2 +- .../google/firebase/projectmanagement/AndroidAppMetadata.java | 2 +- .../google/firebase/projectmanagement/AndroidAppService.java | 2 +- .../firebase/projectmanagement/FirebaseProjectManagement.java | 2 +- .../projectmanagement/FirebaseProjectManagementException.java | 2 +- .../projectmanagement/FirebaseProjectManagementServiceImpl.java | 2 +- .../java/com/google/firebase/projectmanagement/HttpHelper.java | 2 +- src/main/java/com/google/firebase/projectmanagement/IosApp.java | 2 +- .../com/google/firebase/projectmanagement/IosAppMetadata.java | 2 +- .../com/google/firebase/projectmanagement/IosAppService.java | 2 +- .../java/com/google/firebase/projectmanagement/Scheduler.java | 2 +- .../com/google/firebase/projectmanagement/ShaCertificate.java | 2 +- .../google/firebase/projectmanagement/ShaCertificateType.java | 2 +- src/main/resources/admin_sdk.properties | 2 +- src/test/java/com/google/firebase/CodeCoverageReporter.java | 2 +- src/test/java/com/google/firebase/FirebaseAppTest.java | 2 +- src/test/java/com/google/firebase/FirebaseExceptionTest.java | 2 +- src/test/java/com/google/firebase/FirebaseOptionsTest.java | 2 +- src/test/java/com/google/firebase/IncomingHttpResponseTest.java | 2 +- src/test/java/com/google/firebase/OutgoingHttpRequestTest.java | 2 +- .../com/google/firebase/TestOnlyImplFirebaseTrampolines.java | 2 +- src/test/java/com/google/firebase/ThreadManagerTest.java | 2 +- .../java/com/google/firebase/auth/ActionCodeSettingsTest.java | 2 +- .../firebase/auth/EmulatorFirebaseTokenVerifierImplTest.java | 2 +- src/test/java/com/google/firebase/auth/FirebaseAuthIT.java | 2 +- src/test/java/com/google/firebase/auth/FirebaseAuthTest.java | 2 +- .../java/com/google/firebase/auth/FirebaseCustomTokenTest.java | 2 +- src/test/java/com/google/firebase/auth/FirebaseTokenTest.java | 2 +- .../java/com/google/firebase/auth/FirebaseTokenUtilsTest.java | 2 +- .../com/google/firebase/auth/FirebaseTokenVerifierImplTest.java | 2 +- .../java/com/google/firebase/auth/FirebaseUserManagerTest.java | 2 +- src/test/java/com/google/firebase/auth/GetUsersIT.java | 2 +- .../java/com/google/firebase/auth/ImportUserRecordTest.java | 2 +- src/test/java/com/google/firebase/auth/ListUsersPageTest.java | 2 +- .../java/com/google/firebase/auth/MockGoogleCredentials.java | 2 +- src/test/java/com/google/firebase/auth/TestTokenFactory.java | 2 +- src/test/java/com/google/firebase/auth/UserImportHashTest.java | 2 +- .../java/com/google/firebase/auth/UserImportOptionsTest.java | 2 +- src/test/java/com/google/firebase/auth/UserProviderTest.java | 2 +- .../java/com/google/firebase/auth/hash/InvalidHashTest.java | 2 +- .../com/google/firebase/auth/internal/CryptoSignersTest.java | 2 +- .../google/firebase/auth/internal/FirebaseTokenFactoryTest.java | 2 +- src/test/java/com/google/firebase/cloud/StorageClientIT.java | 2 +- src/test/java/com/google/firebase/cloud/StorageClientTest.java | 2 +- .../java/com/google/firebase/database/DataSnapshotTest.java | 2 +- .../com/google/firebase/database/DatabaseReferenceTest.java | 2 +- src/test/java/com/google/firebase/database/EventRecord.java | 2 +- .../java/com/google/firebase/database/FirebaseDatabaseTest.java | 2 +- src/test/java/com/google/firebase/database/MapBuilder.java | 2 +- src/test/java/com/google/firebase/database/MapperTest.java | 2 +- src/test/java/com/google/firebase/database/MutableDataTest.java | 2 +- src/test/java/com/google/firebase/database/ObjectMapTest.java | 2 +- .../java/com/google/firebase/database/OnDisconnectTest.java | 2 +- .../com/google/firebase/database/TestChildEventListener.java | 2 +- src/test/java/com/google/firebase/database/TestFailure.java | 2 +- src/test/java/com/google/firebase/database/TestHelpers.java | 2 +- .../java/com/google/firebase/database/TestTokenProvider.java | 2 +- src/test/java/com/google/firebase/database/UtilitiesTest.java | 2 +- .../com/google/firebase/database/ValueExpectationHelper.java | 2 +- .../google/firebase/database/collection/ArraySortedMapTest.java | 2 +- .../firebase/database/collection/RBTreeSortedMapTest.java | 2 +- .../com/google/firebase/database/connection/ConnectionTest.java | 2 +- .../firebase/database/connection/ConnectionUtilsTest.java | 2 +- .../google/firebase/database/connection/ListenAggregator.java | 2 +- .../firebase/database/connection/WebsocketConnectionTest.java | 2 +- .../com/google/firebase/database/core/CompoundWriteTest.java | 2 +- .../java/com/google/firebase/database/core/CoreTestHelpers.java | 2 +- .../google/firebase/database/core/JvmAuthTokenProviderTest.java | 2 +- .../java/com/google/firebase/database/core/JvmPlatformTest.java | 2 +- src/test/java/com/google/firebase/database/core/PathTest.java | 2 +- .../google/firebase/database/core/RandomOperationGenerator.java | 2 +- .../google/firebase/database/core/RandomViewProcessorTest.java | 2 +- .../java/com/google/firebase/database/core/RangeMergeTest.java | 2 +- .../java/com/google/firebase/database/core/RepoInfoTest.java | 2 +- .../java/com/google/firebase/database/core/SyncPointTest.java | 2 +- .../java/com/google/firebase/database/core/ZombieVerifier.java | 2 +- .../core/persistence/DefaultPersistenceManagerTest.java | 2 +- .../firebase/database/core/persistence/KeepSyncedTestIT.java | 2 +- .../firebase/database/core/persistence/MockListenProvider.java | 2 +- .../database/core/persistence/MockPersistenceStorageEngine.java | 2 +- .../firebase/database/core/persistence/PruneForestTest.java | 2 +- .../database/core/persistence/RandomPersistenceTest.java | 2 +- .../firebase/database/core/persistence/TestCachePolicy.java | 2 +- .../database/core/persistence/TrackedQueryManagerTest.java | 2 +- .../com/google/firebase/database/core/utilities/TestClock.java | 2 +- .../com/google/firebase/database/core/utilities/TreeTest.java | 2 +- .../com/google/firebase/database/core/view/QueryParamsTest.java | 2 +- .../java/com/google/firebase/database/core/view/ViewAccess.java | 2 +- .../java/com/google/firebase/database/future/ReadFuture.java | 2 +- .../java/com/google/firebase/database/future/WriteFuture.java | 2 +- .../com/google/firebase/database/integration/DataTestIT.java | 2 +- .../com/google/firebase/database/integration/EventHelper.java | 2 +- .../com/google/firebase/database/integration/EventTestIT.java | 2 +- .../database/integration/FirebaseDatabaseAuthTestIT.java | 2 +- .../firebase/database/integration/FirebaseDatabaseTestIT.java | 2 +- .../com/google/firebase/database/integration/InfoTestIT.java | 2 +- .../com/google/firebase/database/integration/OrderByTestIT.java | 2 +- .../com/google/firebase/database/integration/OrderTestIT.java | 2 +- .../com/google/firebase/database/integration/QueryTestIT.java | 2 +- .../google/firebase/database/integration/RealtimeTestIT.java | 2 +- .../com/google/firebase/database/integration/RulesClient.java | 2 +- .../com/google/firebase/database/integration/RulesTestIT.java | 2 +- .../google/firebase/database/integration/ShutdownExample.java | 2 +- .../google/firebase/database/integration/TransactionTestIT.java | 2 +- .../com/google/firebase/database/snapshot/CompoundHashTest.java | 2 +- .../java/com/google/firebase/database/snapshot/NodeTest.java | 2 +- .../java/com/google/firebase/database/util/GAuthTokenTest.java | 2 +- .../java/com/google/firebase/database/util/JsonMapperTest.java | 2 +- .../google/firebase/database/utilities/DefaultRunLoopTest.java | 2 +- .../firebase/database/utilities/NodeSizeEstimatorTest.java | 2 +- .../java/com/google/firebase/database/utilities/PairTest.java | 2 +- .../google/firebase/database/utilities/PushIdGeneratorTest.java | 2 +- .../com/google/firebase/database/utilities/UtilitiesTest.java | 2 +- .../com/google/firebase/database/utilities/ValidationTest.java | 2 +- src/test/java/com/google/firebase/iid/FirebaseInstanceIdIT.java | 2 +- .../java/com/google/firebase/iid/FirebaseInstanceIdTest.java | 2 +- .../firebase/internal/AbstractPlatformErrorHandlerTest.java | 2 +- .../com/google/firebase/internal/ApacheHttp2TransportIT.java | 2 +- .../com/google/firebase/internal/ApacheHttp2TransportTest.java | 2 +- .../java/com/google/firebase/internal/ApiClientUtilsTest.java | 2 +- .../com/google/firebase/internal/CallableOperationTest.java | 2 +- .../google/firebase/internal/CountingLowLevelHttpRequest.java | 2 +- src/test/java/com/google/firebase/internal/DateUtilsTest.java | 2 +- .../google/firebase/internal/ErrorHandlingHttpClientTest.java | 2 +- .../java/com/google/firebase/internal/FirebaseAppStoreTest.java | 2 +- .../firebase/internal/FirebaseRequestInitializerTest.java | 2 +- .../google/firebase/internal/FirebaseThreadManagersTest.java | 2 +- .../com/google/firebase/internal/MockApacheHttp2Response.java | 2 +- src/test/java/com/google/firebase/internal/RetryConfigTest.java | 2 +- .../java/com/google/firebase/internal/RetryInitializerTest.java | 2 +- .../firebase/internal/RetryUnsuccessfulResponseHandlerTest.java | 2 +- .../java/com/google/firebase/internal/TestApiClientUtils.java | 2 +- .../java/com/google/firebase/messaging/BatchResponseTest.java | 2 +- .../firebase/messaging/FirebaseMessagingClientImplTest.java | 2 +- .../java/com/google/firebase/messaging/FirebaseMessagingIT.java | 2 +- .../com/google/firebase/messaging/FirebaseMessagingTest.java | 2 +- .../com/google/firebase/messaging/InstanceIdClientImplTest.java | 2 +- src/test/java/com/google/firebase/messaging/MessageTest.java | 2 +- .../com/google/firebase/messaging/MulticastMessageTest.java | 2 +- .../java/com/google/firebase/messaging/SendResponseTest.java | 2 +- .../com/google/firebase/projectmanagement/AndroidAppTest.java | 2 +- .../firebase/projectmanagement/FirebaseProjectManagementIT.java | 2 +- .../FirebaseProjectManagementServiceImplTest.java | 2 +- .../projectmanagement/FirebaseProjectManagementTest.java | 2 +- .../java/com/google/firebase/projectmanagement/IosAppTest.java | 2 +- .../google/firebase/projectmanagement/ShaCertificateTest.java | 2 +- .../java/com/google/firebase/snippets/FirebaseAppSnippets.java | 2 +- .../java/com/google/firebase/snippets/FirebaseAuthSnippets.java | 2 +- .../com/google/firebase/snippets/FirebaseDatabaseSnippets.java | 2 +- .../com/google/firebase/snippets/FirebaseMessagingSnippets.java | 2 +- .../com/google/firebase/snippets/FirebaseStorageSnippets.java | 2 +- src/test/java/com/google/firebase/testing/FirebaseAppRule.java | 2 +- src/test/java/com/google/firebase/testing/GenericFunction.java | 2 +- .../java/com/google/firebase/testing/IntegrationTestUtils.java | 2 +- src/test/java/com/google/firebase/testing/MockitoTestRule.java | 2 +- .../google/firebase/testing/MultiRequestMockHttpTransport.java | 2 +- src/test/java/com/google/firebase/testing/ServiceAccount.java | 2 +- .../com/google/firebase/testing/TestResponseInterceptor.java | 2 +- src/test/java/com/google/firebase/testing/TestUtils.java | 2 +- 450 files changed, 450 insertions(+), 450 deletions(-) diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh index e393f40e4..a0afa6e41 100755 --- a/.github/scripts/generate_changelog.sh +++ b/.github/scripts/generate_changelog.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# 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. diff --git a/.github/scripts/package_artifacts.sh b/.github/scripts/package_artifacts.sh index 30d998c75..fbbc5555b 100755 --- a/.github/scripts/package_artifacts.sh +++ b/.github/scripts/package_artifacts.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# 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. diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh index 8e20af88f..5da0f32a4 100755 --- a/.github/scripts/publish_artifacts.sh +++ b/.github/scripts/publish_artifacts.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# 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. diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh index 3dda8a367..7c23eba57 100755 --- a/.github/scripts/publish_preflight_check.sh +++ b/.github/scripts/publish_preflight_check.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# 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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c26cf2282..4c3d6ac73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -# Copyright 2020 Google Inc. +# 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. diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f7a9d9894..5026f2580 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,4 +1,4 @@ -# Copyright 2021 Google Inc. +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a4c91f91..144a77ca8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# Copyright 2020 Google Inc. +# 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. diff --git a/NOTICE.txt b/NOTICE.txt index b6c5d02b2..f876712e4 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Firebase Admin Java SDK -Copyright 2019 Google Inc. +Copyright 2019 Google LLC This product includes software developed at The Apache Software Foundation (http://www.apache.org/). diff --git a/checkstyle.xml b/checkstyle.xml index 77e2dba54..c2a35946a 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,6 +1,6 @@ maven-compiler-plugin - 3.14.1 + 3.15.0 1.8 1.8 From b095a066e97c53e38c328515312e75701dbfe3e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:30:56 -0500 Subject: [PATCH 192/219] chore(deps): Bump netty.version from 4.2.9.Final to 4.2.10.Final (#1178) Bumps `netty.version` from 4.2.9.Final to 4.2.10.Final. Updates `io.netty:netty-codec-http` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-handler` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 06f42875d..e51fbdabd 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.9.Final + 4.2.10.Final From 2d66ae93ef62e8e6c3d3a7a24dbb7684319ad747 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:27:07 -0500 Subject: [PATCH 193/219] fix: Reimplement HTTP/2 response entity consumption using ApacheHttp2AsyncEntityConsumer and ApacheHttp2Entity (#1181) * fix: Reimplement HTTP/2 response entity consumption using ApacheHttp2AsyncEntityConsumer and ApacheHttp2Entity * fix: Address gemini review --- .../ApacheHttp2AsyncEntityConsumer.java | 85 ++++++++++++++ .../firebase/internal/ApacheHttp2Entity.java | 37 ++++++ .../firebase/internal/ApacheHttp2Request.java | 28 +++-- .../internal/ApacheHttp2Response.java | 45 +++++--- .../internal/ApacheHttp2TransportTest.java | 105 +++++++++++++++++- 5 files changed, 269 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java new file mode 100644 index 000000000..ee8733586 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.nio.AsyncEntityConsumer; +import org.apache.hc.core5.http.nio.CapacityChannel; + +public class ApacheHttp2AsyncEntityConsumer implements AsyncEntityConsumer { + + private EntityDetails entityDetails; + private FutureCallback resultCallback; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + @Override + public void streamStart( + final EntityDetails entityDetails, + final FutureCallback resultCallback) + throws HttpException, IOException { + this.entityDetails = entityDetails; + this.resultCallback = resultCallback; + } + + @Override + public void updateCapacity(CapacityChannel capacityChannel) throws IOException { + capacityChannel.update(Integer.MAX_VALUE); + } + + @Override + public void consume(ByteBuffer src) throws IOException { + if (src.hasArray()) { + buffer.write(src.array(), src.arrayOffset() + src.position(), src.remaining()); + src.position(src.limit()); + } else { + byte[] bytes = new byte[src.remaining()]; + src.get(bytes); + buffer.write(bytes); + } + } + + @Override + public void streamEnd(List trailers) throws HttpException, IOException { + if (resultCallback != null) { + resultCallback.completed(getContent()); + } + } + + @Override + public void failed(Exception cause) { + if (resultCallback != null) { + resultCallback.failed(cause); + } + } + + @Override + public void releaseResources() { + buffer.reset(); + } + + @Override + public ApacheHttp2Entity getContent() { + return new ApacheHttp2Entity(buffer.toByteArray(), entityDetails); + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java new file mode 100644 index 000000000..9769a28a8 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.internal; + +import org.apache.hc.core5.http.EntityDetails; + +public class ApacheHttp2Entity { + private final byte[] content; + private final EntityDetails entityDetails; + + public ApacheHttp2Entity(byte[] content, EntityDetails entityDetails) { + this.content = content; + this.entityDetails = entityDetails; + } + + public byte[] getContent() { + return content; + } + + public EntityDetails getEntityDetails() { + return entityDetails; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index a711dc0ac..ceb213c52 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -32,13 +32,14 @@ import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.client5.http.HttpHostConnectException; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; -import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; -import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http2.H2StreamResetException; import org.apache.hc.core5.util.Timeout; @@ -49,6 +50,7 @@ final class ApacheHttp2Request extends LowLevelHttpRequest { private final RequestConfig.Builder requestConfig; private int writeTimeout; private ApacheHttp2AsyncEntityProducer entityProducer; + private ApacheHttp2AsyncEntityConsumer entityConsumer; ApacheHttp2Request( CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { @@ -85,17 +87,20 @@ public LowLevelHttpResponse execute() throws IOException { // Build request request = requestBuilder.build(); - // Make Producer + // Make Entity Producer CompletableFuture writeFuture = new CompletableFuture<>(); entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture); + // Make Entity Consumer + entityConsumer = new ApacheHttp2AsyncEntityConsumer(); + // Execute - final Future responseFuture = httpAsyncClient.execute( + final Future> responseFuture = httpAsyncClient.execute( new BasicRequestProducer(request, entityProducer), - SimpleResponseConsumer.create(), - new FutureCallback() { + new BasicResponseConsumer(entityConsumer), + new FutureCallback>() { @Override - public void completed(final SimpleHttpResponse response) { + public void completed(final Message response) { } @Override @@ -120,10 +125,10 @@ public void cancelled() { // Wait for response try { - final SimpleHttpResponse response = responseFuture.get(); + final Message response = responseFuture.get(); return new ApacheHttp2Response(response); } catch (ExecutionException e) { - if (e.getCause() instanceof ConnectTimeoutException + if (e.getCause() instanceof ConnectTimeoutException || e.getCause() instanceof SocketTimeoutException) { throw new IOException("Connection Timeout", e.getCause()); } else if (e.getCause() instanceof HttpHostConnectException) { @@ -144,4 +149,9 @@ public void cancelled() { ApacheHttp2AsyncEntityProducer getEntityProducer() { return entityProducer; } + + @VisibleForTesting + ApacheHttp2AsyncEntityConsumer getEntityConsumer() { + return entityConsumer; + } } diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java index 329ec3970..7d277b972 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java @@ -23,17 +23,27 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; -import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.message.StatusLine; public class ApacheHttp2Response extends LowLevelHttpResponse { - private final SimpleHttpResponse response; + private final Message message; + private final HttpResponse response; private final Header[] allHeaders; + private final EntityDetails entity; + private final byte[] content; - ApacheHttp2Response(SimpleHttpResponse response) { - this.response = response; - allHeaders = response.getHeaders(); + ApacheHttp2Response(Message message) { + this.message = message; + this.response = message.getHead(); + this.allHeaders = response.getHeaders(); + + ApacheHttp2Entity body = message.getBody(); + this.entity = body != null ? body.getEntityDetails() : null; + this.content = body != null ? body.getContent() : null; } @Override @@ -43,25 +53,25 @@ public int getStatusCode() { @Override public InputStream getContent() throws IOException { - return new ByteArrayInputStream(response.getBodyBytes()); + return content == null ? null : new ByteArrayInputStream(content); } @Override public String getContentEncoding() { - Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); - return contentEncodingHeader == null ? null : contentEncodingHeader.getValue(); + return entity == null ? null : entity.getContentEncoding(); } @Override public long getContentLength() { - String bodyText = response.getBodyText(); - return bodyText == null ? 0 : bodyText.length(); + if (content != null) { + return content.length; + } + return entity == null ? -1 : entity.getContentLength(); } @Override public String getContentType() { - ContentType contentType = response.getContentType(); - return contentType == null ? null : contentType.toString(); + return entity == null ? null : entity.getContentType(); } @Override @@ -71,11 +81,12 @@ public String getReasonPhrase() { @Override public String getStatusLine() { - return response.toString(); + return new StatusLine(response).toString(); } public String getHeaderValue(String name) { - return response.getLastHeader(name).getValue(); + Header header = response.getLastHeader(name); + return header == null ? null : header.getValue(); } @Override @@ -94,7 +105,7 @@ public String getHeaderName(int index) { } @VisibleForTesting - public SimpleHttpResponse getResponse() { - return response; + public Message getMessage() { + return message; } } diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java index 60b053109..ba604d3d1 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java @@ -56,11 +56,13 @@ import org.apache.hc.core5.http.HttpRequestMapper; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.HttpServer; import org.apache.hc.core5.http.impl.io.HttpService; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler; +import org.apache.hc.core5.http.message.BasicHttpResponse; import org.apache.hc.core5.http.nio.AsyncPushConsumer; import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.apache.hc.core5.http.nio.AsyncResponseConsumer; @@ -87,7 +89,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + return (Future) CompletableFuture + .completedFuture( + new Message(new BasicHttpResponse(200), null)); } }, requestBuilder); @@ -118,7 +122,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + return (Future) CompletableFuture + .completedFuture( + new Message(new BasicHttpResponse(200), null)); } }, requestBuilder); @@ -148,7 +154,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(simpleHttpResponse); + return (Future) CompletableFuture + .completedFuture(new Message(simpleHttpResponse, + new ApacheHttp2Entity(simpleHttpResponse.getBodyBytes(), null))); } }, requestBuilder); LowLevelHttpResponse response = request.execute(); @@ -157,7 +165,7 @@ public Future doExecute( // we confirm that the simple response we prepared in this test is the same as // the content's response assertTrue(response.getContent() instanceof ByteArrayInputStream); - assertEquals(simpleHttpResponse, ((ApacheHttp2Response) response).getResponse()); + assertEquals(simpleHttpResponse, ((ApacheHttp2Response) response).getMessage().getHead()); // No need to cloase ByteArrayInputStream since close() has no effect. } @@ -212,7 +220,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + return (Future) CompletableFuture + .completedFuture(new Message( + new BasicHttpResponse(200), null)); } }; ApacheHttp2Transport transport = new ApacheHttp2Transport(mockClient); @@ -328,6 +338,91 @@ private void execute(ApacheHttp2Request request) throws IOException { request.execute(); } + @Test + public void testGzipResponse() throws IOException { + final String originalContent = "hello world"; + final java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + try (java.util.zip.GZIPOutputStream gzip = new java.util.zip.GZIPOutputStream(baos)) { + gzip.write(originalContent.getBytes(StandardCharsets.UTF_8)); + } + final byte[] gzippedContent = baos.toByteArray(); + + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(gzippedContent.length)); + response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8"); + ByteArrayEntity entity = new ByteArrayEntity(gzippedContent, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo"); + testUrl.setPort(server.getPort()); + + // Execute the low-level request directly to accurately assert metadata + // without Google's parsed HttpHeaders map mangling the payload lengths. + ApacheHttp2Request request = transport.buildRequest("GET", testUrl.build()); + LowLevelHttpResponse response = request.execute(); + + assertEquals(200, response.getStatusCode()); + assertEquals("text/plain; charset=UTF-8", response.getContentType()); + + boolean wasAutoDecompressed = response.getContentEncoding() == null; + if (wasAutoDecompressed) { + assertEquals(originalContent.length(), response.getContentLength()); + } else { + assertEquals("gzip", response.getContentEncoding()); + assertEquals(gzippedContent.length, response.getContentLength()); + } + + // Verify the low-level stream returns the exact expected payload based on + // decompression state + java.io.InputStream stream = response.getContent(); + byte[] resultBytes = com.google.common.io.ByteStreams.toByteArray(stream); + + if (wasAutoDecompressed) { + assertEquals(originalContent, new String(resultBytes, StandardCharsets.UTF_8)); + } else { + org.junit.Assert.assertArrayEquals(gzippedContent, resultBytes); + } + } + } + + @Test + public void testEmptyResponseWithHeaders() throws IOException { + // Tests that a response with no actual body but headers does not throw NPE + // in ApacheHttp2Response due to entity being null. + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + response.setCode(HttpStatus.SC_NO_CONTENT); + response.setHeader(HttpHeaders.CONTENT_LENGTH, "0"); + // Explicitly omitting the entity to simulate NO_CONTENT bodyless response + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/empty"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpResponse response = transport.createRequestFactory() + .buildGetRequest(testUrl) + .execute(); + + assertEquals(204, response.getStatusCode()); + assertEquals(0L, response.getHeaders().getContentLength().longValue()); + } + } + private static class FakeServer implements AutoCloseable { private final HttpServer server; From b29ac825c7d78b7aa17c01ee0775352cdbd6c849 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:45:39 -0500 Subject: [PATCH 194/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.75.0 to 26.76.0 (#1185) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.75.0 to 26.76.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.75.0...v26.76.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.76.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e51fbdabd..8c0093c0f 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.75.0 + 26.76.0 pom import From a568b65964a596a78e1b3d468c9e7243778d6fe7 Mon Sep 17 00:00:00 2001 From: Ashish Kothari Date: Wed, 25 Feb 2026 21:57:46 +0530 Subject: [PATCH 195/219] Add javadocs to public methods (#1187) --- .../firebase/remoteconfig/ParameterValue.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java index 7bf9d3ba0..db63e3718 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java +++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java @@ -205,14 +205,30 @@ private RolloutValue(String rolloutId, String value, double percent) { this.percent = percent; } + /** + * Gets the ID of the Rollout linked to this value. + * + * @return The Rollout ID + */ public String getRolloutId() { return rolloutId; } + /** + * Gets the value that is being rolled out. + * + * @return The rollout value + */ public String getValue() { return value; } + /** + * Gets the rollout percentage representing the exposure of rollout value + * in the target audience. + * + * @return Percentage of audience exposed to the rollout + */ public double getPercent() { return percent; } @@ -256,6 +272,11 @@ private PersonalizationValue(String personalizationId) { this.personalizationId = personalizationId; } + /** + * Gets the ID of the Personalization linked to this value. + * + * @return The Personalization ID + */ public String getPersonalizationId() { return personalizationId; } @@ -320,10 +341,20 @@ public static ExperimentVariantValue ofNoChange(String variantId) { return new ExperimentVariantValue(variantId, null, true); } + /** + * Gets the ID of the experiment variant. + * + * @return The variant ID + */ public String getVariantId() { return variantId; } + /** + * Gets the value of the experiment variant. + * + * @return The variant value + */ @Nullable public String getValue() { return value; @@ -334,6 +365,11 @@ Boolean getNoChange() { return noChange; } + /** + * Returns whether the experiment variant is a no-change variant. + * + * @return true if the experiment variant is a no-change variant, and false otherwise. + */ public boolean isNoChange() { return Boolean.TRUE.equals(noChange); } @@ -370,10 +406,20 @@ private ExperimentValue(String experimentId, List varian this.variantValues = variantValues; } + /** + * Gets the ID of the experiment linked to this value. + * + * @return The Experiment ID + */ public String getExperimentId() { return experimentId; } + /** + * Gets a collection of variant values served by the experiment. + * + * @return List of {@link ExperimentVariantValue} + */ public List getExperimentVariantValues() { return variantValues; } From 5169aae2642a27ca8d423aa28365ca111d34a4d2 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:39:22 -0500 Subject: [PATCH 196/219] [chore] Release 9.8.0 (#1184) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8c0093c0f..a642c1936 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.7.1 + 9.8.0 jar firebase-admin From d69dcbd9604ca2eb49c32d27067df4b70a126c87 Mon Sep 17 00:00:00 2001 From: andrew-signal Date: Tue, 3 Mar 2026 12:06:07 -0500 Subject: [PATCH 197/219] Add support for new bandwidthConstrainedOk flag used to deliver notifications to sat networks (#1145) Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --- .../firebase/messaging/AndroidConfig.java | 14 +++++++++++ .../firebase/messaging/MessageTest.java | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index ac9d8a52f..941793479 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -55,6 +55,9 @@ public class AndroidConfig { @Key("direct_boot_ok") private final Boolean directBootOk; + @Key("bandwidth_constrained_ok") + private final Boolean bandwidthConstrainedOk; + private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; if (builder.priority != null) { @@ -79,6 +82,7 @@ private AndroidConfig(Builder builder) { this.notification = builder.notification; this.fcmOptions = builder.fcmOptions; this.directBootOk = builder.directBootOk; + this.bandwidthConstrainedOk = builder.bandwidthConstrainedOk; } /** @@ -108,6 +112,7 @@ public static class Builder { private AndroidNotification notification; private AndroidFcmOptions fcmOptions; private Boolean directBootOk; + private Boolean bandwidthConstrainedOk; private Builder() {} @@ -218,6 +223,15 @@ public Builder setDirectBootOk(boolean directBootOk) { return this; } + /** + * Sets the {@code bandwidth_constrained_ok} flag. If set to true, messages can be delivered + * even when the device is connected through a bandwidth-constrained network. + */ + public Builder setBandwidthConstrainedOk(boolean bandwidthConstrainedOk) { + this.bandwidthConstrainedOk = bandwidthConstrainedOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 263d1ced5..70d130032 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -224,6 +224,29 @@ public void testAndroidMessageWithDirectBootOk() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithBandwidthConstrainedOk() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setBandwidthConstrainedOk(true) + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .build(); + Map data = ImmutableMap.of( + "bandwidth_constrained_ok", true, + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test(expected = IllegalArgumentException.class) public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { AndroidNotification.builder().setNotificationCount(-1).build(); From d04e15b73b4972c1e6e50ed14ca1c2956e761266 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:48:34 -0400 Subject: [PATCH 198/219] feat(fcm): Add support for restricted satellite API (#1191) --- .../firebase/messaging/AndroidConfig.java | 18 +++++++++++-- .../firebase/messaging/MessageTest.java | 26 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 941793479..22f591680 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -58,6 +58,9 @@ public class AndroidConfig { @Key("bandwidth_constrained_ok") private final Boolean bandwidthConstrainedOk; + @Key("restricted_satellite_ok") + private final Boolean restrictedSatelliteOk; + private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; if (builder.priority != null) { @@ -83,6 +86,7 @@ private AndroidConfig(Builder builder) { this.fcmOptions = builder.fcmOptions; this.directBootOk = builder.directBootOk; this.bandwidthConstrainedOk = builder.bandwidthConstrainedOk; + this.restrictedSatelliteOk = builder.restrictedSatelliteOk; } /** @@ -113,6 +117,7 @@ public static class Builder { private AndroidFcmOptions fcmOptions; private Boolean directBootOk; private Boolean bandwidthConstrainedOk; + private Boolean restrictedSatelliteOk; private Builder() {} @@ -224,14 +229,23 @@ public Builder setDirectBootOk(boolean directBootOk) { } /** - * Sets the {@code bandwidth_constrained_ok} flag. If set to true, messages can be delivered - * even when the device is connected through a bandwidth-constrained network. + * Sets the {@code bandwidth_constrained_ok} flag. If set to true, messages will be allowed + * to be delivered to the app while the device is on a bandwidth constrained network. */ public Builder setBandwidthConstrainedOk(boolean bandwidthConstrainedOk) { this.bandwidthConstrainedOk = bandwidthConstrainedOk; return this; } + /** + * Sets the {@code restricted_satellite_ok} flag. If set to true, messages will be allowed + * to be delivered to the app while the device is on a restricted satellite network. + */ + public Builder setRestrictedSatelliteOk(boolean restrictedSatelliteOk) { + this.restrictedSatelliteOk = restrictedSatelliteOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 70d130032..94ca4fa4a 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; -import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonParser; import com.google.common.collect.ImmutableList; @@ -247,6 +246,29 @@ public void testAndroidMessageWithBandwidthConstrainedOk() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithRestrictedSatelliteOk() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setRestrictedSatelliteOk(true) + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .build(); + Map data = ImmutableMap.of( + "restricted_satellite_ok", true, + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test(expected = IllegalArgumentException.class) public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { AndroidNotification.builder().setNotificationCount(-1).build(); @@ -991,7 +1013,7 @@ public void testExtendedAndroidNotificationParameters() throws IOException { } private static void assertJsonEquals( - Map expected, Object actual) throws IOException { + Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); } From ab858ef2004c69c1ffee5a7dbb6144618646e206 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:28:26 +0530 Subject: [PATCH 199/219] feat(rc):Add exposurePercent to ExperimentValues (#1201) * Add exposurePercent to ExperimentValues * fix comments * fix build * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix build * fix build * fix build * fix build * fix build * fix build * fix build * resolved comments * resolved comments * resolved comments * resolved comments * Update TemplateResponse.java * Update Template.java --------- Co-authored-by: Varun Rathore Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../firebase/remoteconfig/ParameterValue.java | 133 ++++++++++-------- .../firebase/remoteconfig/Template.java | 58 +++++++- .../internal/TemplateResponse.java | 12 ++ .../remoteconfig/ParameterValueTest.java | 34 ++++- .../firebase/remoteconfig/TemplateTest.java | 2 +- 5 files changed, 172 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java index db63e3718..1d48bbb03 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java +++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java @@ -26,14 +26,12 @@ import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.PersonalizationValueResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.RolloutValueResponse; - -import java.util.ArrayList; import java.util.List; import java.util.Objects; -/** - * Represents a Remote Config parameter value that can be used in a {@link Template}. - */ +/** Represents a Remote Config parameter value + * that can be used in a + * {@link Template}. */ public abstract class ParameterValue { /** @@ -82,17 +80,18 @@ public static PersonalizationValue ofPersonalization(String personalizationId) { * * @param experimentId The experiment ID. * @param variantValues The list of experiment variant values. + * @param exposurePercent The exposure percentage of the experiment. * @return A {@link ParameterValue.ExperimentValue} instance. */ - public static ExperimentValue ofExperiment(String experimentId, - List variantValues) { - return new ExperimentValue(experimentId, variantValues); + public static ExperimentValue ofExperiment( + String experimentId, List variantValues, double exposurePercent) { + return new ExperimentValue(experimentId, variantValues, exposurePercent); } abstract ParameterValueResponse toParameterValueResponse(); static ParameterValue fromParameterValueResponse( - @NonNull ParameterValueResponse parameterValueResponse) { + @NonNull ParameterValueResponse parameterValueResponse) { checkNotNull(parameterValueResponse); if (parameterValueResponse.isUseInAppDefault()) { return ParameterValue.inAppDefault(); @@ -102,7 +101,7 @@ static ParameterValue fromParameterValueResponse( // Protobuf serialization does not set values for fields on the wire when // they are equal to the default value for the field type. When deserializing, // can appear as the value not being set. Explicitly handle default value for - // the percent field since 0 is a valid value. + // the percent field since 0 is a valid value. double percent = 0; if (rv.getPercent() != null) { percent = rv.getPercent(); @@ -115,18 +114,26 @@ static ParameterValue fromParameterValueResponse( } if (parameterValueResponse.getExperimentValue() != null) { ExperimentValueResponse ev = parameterValueResponse.getExperimentValue(); - List variantValues = ev.getExperimentVariantValues().stream() - .map(evv -> new ExperimentVariantValue( - evv.getVariantId(), evv.getValue(), evv.getNoChange())) - .collect(toList()); - return ParameterValue.ofExperiment(ev.getExperimentId(), variantValues); + List variantValues = + ev.getExperimentVariantValues().stream() + .map( + evv -> + new ExperimentVariantValue( + evv.getVariantId(), evv.getValue(), evv.getNoChange())) + .collect(toList()); + // Handle null exposurePercent by defaulting to 0 + double exposurePercent = 0; + if (ev.getExposurePercent() != null) { + exposurePercent = ev.getExposurePercent(); + } + return ParameterValue.ofExperiment( + ev.getExperimentId(), variantValues, exposurePercent); } return ParameterValue.of(parameterValueResponse.getValue()); } /** - * Represents an explicit Remote Config parameter value with a value that the - * parameter is set to. + * Represents an explicit Remote Config parameter value with a value that the parameter is set to. */ public static final class Explicit extends ParameterValue { @@ -147,8 +154,7 @@ public String getValue() { @Override ParameterValueResponse toParameterValueResponse() { - return new ParameterValueResponse() - .setValue(this.value); + return new ParameterValueResponse().setValue(this.value); } @Override @@ -169,9 +175,7 @@ public int hashCode() { } } - /** - * Represents an in app default parameter value. - */ + /** Represents an in app default parameter value. */ public static final class InAppDefault extends ParameterValue { @Override @@ -191,9 +195,7 @@ public boolean equals(Object o) { } } - /** - * Represents a Rollout value. - */ + /** Represents a Rollout value. */ public static final class RolloutValue extends ParameterValue { private final String rolloutId; private final String value; @@ -224,8 +226,8 @@ public String getValue() { } /** - * Gets the rollout percentage representing the exposure of rollout value - * in the target audience. + * Gets the rollout percentage representing the exposure of rollout value in the target + * audience. * * @return Percentage of audience exposed to the rollout */ @@ -235,11 +237,12 @@ public double getPercent() { @Override ParameterValueResponse toParameterValueResponse() { - return new ParameterValueResponse().setRolloutValue( + return new ParameterValueResponse() + .setRolloutValue( new RolloutValueResponse() - .setRolloutId(this.rolloutId) - .setValue(this.value) - .setPercent(this.percent)); + .setRolloutId(this.rolloutId) + .setValue(this.value) + .setPercent(this.percent)); } @Override @@ -252,8 +255,8 @@ public boolean equals(Object o) { } RolloutValue that = (RolloutValue) o; return Double.compare(that.percent, percent) == 0 - && Objects.equals(rolloutId, that.rolloutId) - && Objects.equals(value, that.value); + && Objects.equals(rolloutId, that.rolloutId) + && Objects.equals(value, that.value); } @Override @@ -262,9 +265,7 @@ public int hashCode() { } } - /** - * Represents a Personalization value. - */ + /** Represents a Personalization value. */ public static final class PersonalizationValue extends ParameterValue { private final String personalizationId; @@ -283,9 +284,9 @@ public String getPersonalizationId() { @Override ParameterValueResponse toParameterValueResponse() { - return new ParameterValueResponse().setPersonalizationValue( - new PersonalizationValueResponse() - .setPersonalizationId(this.personalizationId)); + return new ParameterValueResponse() + .setPersonalizationValue( + new PersonalizationValueResponse().setPersonalizationId(this.personalizationId)); } @Override @@ -306,9 +307,7 @@ public int hashCode() { } } - /** - * Represents a specific variant within an Experiment. - */ + /** Represents a specific variant within an Experiment. */ public static final class ExperimentVariantValue { private final String variantId; private final String value; @@ -384,8 +383,8 @@ public boolean equals(Object o) { } ExperimentVariantValue that = (ExperimentVariantValue) o; return noChange == that.noChange - && Objects.equals(variantId, that.variantId) - && Objects.equals(value, that.value); + && Objects.equals(variantId, that.variantId) + && Objects.equals(value, that.value); } @Override @@ -394,16 +393,17 @@ public int hashCode() { } } - /** - * Represents an Experiment value. - */ + /** Represents an Experiment value. */ public static final class ExperimentValue extends ParameterValue { private final String experimentId; private final List variantValues; + private final double exposurePercent; - private ExperimentValue(String experimentId, List variantValues) { + private ExperimentValue( + String experimentId, List variantValues, double exposurePercent) { this.experimentId = experimentId; this.variantValues = variantValues; + this.exposurePercent = exposurePercent; } /** @@ -415,6 +415,15 @@ public String getExperimentId() { return experimentId; } + /** + * Gets the exposure percentage of the experiment linked to this value. + * + * @return Exposure percentage of the experiment linked to this value. + */ + public double getExposurePercent() { + return exposurePercent; + } + /** * Gets a collection of variant values served by the experiment. * @@ -426,16 +435,21 @@ public List getExperimentVariantValues() { @Override ParameterValueResponse toParameterValueResponse() { - List variantValueResponses = variantValues.stream() - .map(variantValue -> new ExperimentVariantValueResponse() - .setVariantId(variantValue.getVariantId()) - .setValue(variantValue.getValue()) - .setNoChange(variantValue.getNoChange())) - .collect(toList()); - return new ParameterValueResponse().setExperimentValue( + List variantValueResponses = + variantValues.stream() + .map( + variantValue -> + new ExperimentVariantValueResponse() + .setVariantId(variantValue.getVariantId()) + .setValue(variantValue.getValue()) + .setNoChange(variantValue.getNoChange())) + .collect(toList()); + return new ParameterValueResponse() + .setExperimentValue( new ExperimentValueResponse() - .setExperimentId(this.experimentId) - .setExperimentVariantValues(variantValueResponses)); + .setExperimentId(this.experimentId) + .setExperimentVariantValues(variantValueResponses) + .setExposurePercent(this.exposurePercent)); } @Override @@ -448,12 +462,13 @@ public boolean equals(Object o) { } ExperimentValue that = (ExperimentValue) o; return Objects.equals(experimentId, that.experimentId) - && Objects.equals(variantValues, that.variantValues); + && Objects.equals(variantValues, that.variantValues) + && Double.compare(that.exposurePercent, exposurePercent) == 0; } @Override public int hashCode() { - return Objects.hash(experimentId, variantValues); + return Objects.hash(experimentId, variantValues, exposurePercent); } } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index d94cfc89e..5dfe06aaf 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -60,7 +60,7 @@ public Template(String etag) { this((String) null); } - Template(@NonNull TemplateResponse templateResponse) { + Template(@NonNull TemplateResponse templateResponse) throws FirebaseRemoteConfigException { checkNotNull(templateResponse); this.parameters = new HashMap<>(); this.conditions = new ArrayList<>(); @@ -86,6 +86,7 @@ public Template(String etag) { if (templateResponse.getVersion() != null) { this.version = new Version(templateResponse.getVersion()); } + validateExperimentExposurePercents(this.parameters, this.parameterGroups); this.etag = templateResponse.getEtag(); } @@ -278,4 +279,59 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(etag, parameters, conditions, parameterGroups, version); } + + private void validateExperimentExposurePercents( + Map parameters, + Map parameterGroups) throws FirebaseRemoteConfigException { + Map experimentExposurePercents = new HashMap<>(); + validateParameters(parameters, experimentExposurePercents); + if (parameterGroups != null) { + for (ParameterGroup group : parameterGroups.values()) { + validateParameters(group.getParameters(), experimentExposurePercents); + } + } + } + + private void validateParameters( + Map parameters, + Map experimentExposurePercents) throws FirebaseRemoteConfigException { + if (parameters == null) { + return; + } + for (Map.Entry entry : parameters.entrySet()) { + Parameter parameter = entry.getValue(); + String parameterName = entry.getKey(); + checkExposurePercent(parameter.getDefaultValue(), parameterName, experimentExposurePercents); + if (parameter.getConditionalValues() != null) { + for (ParameterValue value : parameter.getConditionalValues().values()) { + checkExposurePercent(value, parameterName, experimentExposurePercents); + } + } + } + } + + private void checkExposurePercent( + ParameterValue value, + String parameterName, + Map experimentExposurePercents) throws FirebaseRemoteConfigException { + if (value instanceof ParameterValue.ExperimentValue) { + ParameterValue.ExperimentValue experimentValue = (ParameterValue.ExperimentValue) value; + Double exposurePercent = experimentValue.getExposurePercent(); + if (exposurePercent != null) { + // Enforce range [0, 100] + if (exposurePercent < 0 || exposurePercent > 100) { + return; + } + // Enforce consistency for the same experimentId + String experimentId = experimentValue.getExperimentId(); + if (experimentExposurePercents.containsKey(experimentId)) { + if (!Objects.equals(experimentExposurePercents.get(experimentId), exposurePercent)) { + return; + } + } else { + experimentExposurePercents.put(experimentId, exposurePercent); + } + } + } + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index b92580f8c..32f5bcef9 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -288,6 +288,9 @@ public static final class ExperimentValueResponse { @Key("variantValue") private List experimentVariantValues; + @Key("exposurePercent") + private Double exposurePercent; + public String getExperimentId() { return experimentId; } @@ -296,6 +299,10 @@ public List getExperimentVariantValues() { return experimentVariantValues; } + public Double getExposurePercent() { + return exposurePercent; + } + public ExperimentValueResponse setExperimentId(String experimentId) { this.experimentId = experimentId; return this; @@ -306,6 +313,11 @@ public ExperimentValueResponse setExperimentVariantValues( this.experimentVariantValues = experimentVariantValues; return this; } + + public ExperimentValueResponse setExposurePercent(Double exposurePercent) { + this.exposurePercent = exposurePercent; + return this; + } } /** diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java index 91f908bd3..46db5c44e 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.remoteconfig.ParameterValue.ExperimentVariantValue; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; + import org.junit.Test; public class ParameterValueTest { @@ -64,7 +66,11 @@ public void testCreateExperimentValue() { ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1"), ExperimentVariantValue.ofNoChange("variant_2") - )); + ), 10.0); + + assertEquals("experiment_1", parameterValue.getExperimentId()); + assertEquals(2, parameterValue.getExperimentVariantValues().size()); + assertEquals(10.0, parameterValue.getExposurePercent(), 0.0); assertEquals("experiment_1", parameterValue.getExperimentId()); assertEquals(2, parameterValue.getExperimentVariantValues().size()); @@ -77,6 +83,7 @@ public void testCreateExperimentValue() { assertEquals("variant_2", variant2.getVariantId()); assertEquals(null, variant2.getValue()); assertEquals(true, variant2.isNoChange()); + } @Test @@ -116,22 +123,37 @@ public void testEquality() { ParameterValue.ExperimentValue experimentValueOne = ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1") - )); + ), 10.0); ParameterValue.ExperimentValue experimentValueTwo = ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1") - )); + ), 10.0); ParameterValue.ExperimentValue experimentValueThree = ParameterValue.ofExperiment("experiment_2", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1") - )); + ), 10.0); ParameterValue.ExperimentValue experimentValueFour = ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_2", "value_2") - )); - + ), 20.0); assertEquals(experimentValueOne, experimentValueTwo); assertNotEquals(experimentValueOne, experimentValueThree); assertNotEquals(experimentValueOne, experimentValueFour); } + + @Test + public void testExperimentValueWithZeroExposure() { + ParameterValue.ExperimentValue value = ParameterValue.ofExperiment( + "exp_0", ImmutableList.of(ExperimentVariantValue.of("v1", "foo")), 0.0); + + // Test Serialization + ParameterValueResponse response = value.toParameterValueResponse(); + assertEquals(0.0, response.getExperimentValue().getExposurePercent(), 0.0); + + // Test Deserialization + ParameterValue fromResponse = ParameterValue.fromParameterValueResponse(response); + assertTrue(fromResponse instanceof ParameterValue.ExperimentValue); + assertEquals(0.0, ((ParameterValue.ExperimentValue) fromResponse).getExposurePercent(), 0.0); + } + } diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index a3ea3e878..92abb850c 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -107,7 +107,7 @@ public void testConstructorWithETag() { } @Test(expected = NullPointerException.class) - public void testConstructorWithNullTemplateResponse() { + public void testConstructorWithNullTemplateResponse() throws FirebaseRemoteConfigException { new Template((TemplateResponse) null); } From 2d7c8d924f458d4e1496e65e5d66e6bdc8e07bb9 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Wed, 13 May 2026 19:02:52 +0530 Subject: [PATCH 200/219] chore: Fix issue in the new RC unit tests (#1205) --- .../remoteconfig/ServerTemplateImpl.java | 2 +- .../remoteconfig/ServerTemplateImplTest.java | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java index 742c19803..633730bf3 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -75,7 +75,7 @@ private ServerTemplateImpl(Builder builder) { try { this.cache.set(ServerTemplateData.fromJSON(initialTemplate)); } catch (FirebaseRemoteConfigException e) { - e.printStackTrace(); + throw new IllegalArgumentException("Unable to parse JSON string.", e); } } diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index bafe84331..d7b761141 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import com.google.api.core.ApiFuture; import com.google.firebase.FirebaseApp; @@ -188,20 +189,17 @@ public void testEvaluateWithoutDefaultValueReturnsEmptyString() public void testEvaluateWithInvalidCacheValueThrowsException() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - KeysAndValues context = new KeysAndValues.Builder().build(); String invalidJsonString = "abc"; - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(invalidJsonString) - .build(); - - FirebaseRemoteConfigException error = - assertThrows(FirebaseRemoteConfigException.class, () -> template.evaluate(context)); - - assertEquals( - "No Remote Config Server template in cache. Call load() before " + "calling evaluate().", - error.getMessage()); + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(invalidJsonString) + .build()); + + assertEquals("Unable to parse JSON string.", error.getMessage()); + // Verify the cause is the original FirebaseRemoteConfigException + assertTrue(error.getCause() instanceof FirebaseRemoteConfigException); } @Test From de3118480af9806b6b9ba880803885f576f56e27 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 13 May 2026 12:44:23 -0400 Subject: [PATCH 201/219] feat(pnv): Add support for Firebase Phone Number Verification (#1203) * feat: add fpnv branch * feat: update pom file * chore: resolve comments * chore: resolve comments * feat: update code and tests * chore: resolve comments * feat: update FirebasePnvException * Update src/main/java/com/google/firebase/fpnv/FirebasePnvToken.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore: resolve robot comments * chore: add test coverage * rename fpnv to fully qualified name * security: enforce project-specific issuer validation in PhoneNumberVerification * chore: handle both Date and Number type safely for token claims * style: wrap long lines to meet checkstyle line width limits * style: resolve checkstyle line length constraints across source and test files * docs: add AGENT.md outlining security and style rules for phone number verification * docs: replace specific agent file with general repo-wide AGENTS.md style rules * fix(remoteconfig): catch parsing runtime exceptions in fromJSON to stabilize headless pipelines * test(phonenumberverification): correct claims setup in wrong audience test * fix(phonenumberverification): use null-safe comparison for header algorithm * revert(remoteconfig): completely restore original state of Template.java * fix(phonenumberverification): replace deprecated URL constructor with URI.create().toURL() * fix(phonenumberverification): implement thread-safe lazy initialization for jwtProcessor to avoid constructor leak * docs(phonenumberverification): add descriptive javadoc to package-private constructor overload * refactor(phonenumberverification): use idiomatic checkNotNull and throw IllegalStateException for hardcoded configuration failures * refactor(phonenumberverification): align token constructor pre-conditions checks with idiomatic checkNotNull patterns * test(phonenumberverification): assert NullPointerException in verifier null claims test case * fix(phonenumberverification): add volatile keyword to jwtProcessor for strict double-checked locking memory safety * Refactor Phone Number Verification error handling to align with SDK standards * Update src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerification.java Co-authored-by: Kevin Cheung --------- Co-authored-by: Andrii Boiko Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Kevin Cheung --- AGENTS.md | 33 ++ pom.xml | 5 + .../FirebasePhoneNumberVerification.java | 84 ++++ ...ebasePhoneNumberVerificationErrorCode.java | 48 ++ ...ebasePhoneNumberVerificationException.java | 51 ++ .../FirebasePhoneNumberVerificationToken.java | 102 ++++ ...ePhoneNumberVerificationTokenVerifier.java | 252 ++++++++++ ...ePhoneNumberVerificationErrorCodeTest.java | 32 ++ .../FirebasePhoneNumberVerificationTest.java | 170 +++++++ ...ebasePhoneNumberVerificationTokenTest.java | 104 ++++ ...neNumberVerificationTokenVerifierTest.java | 456 ++++++++++++++++++ 11 files changed, 1337 insertions(+) create mode 100644 AGENTS.md create mode 100644 src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerification.java create mode 100644 src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCode.java create mode 100644 src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationException.java create mode 100644 src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationToken.java create mode 100644 src/main/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifier.java create mode 100644 src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCodeTest.java create mode 100644 src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTest.java create mode 100644 src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTokenTest.java create mode 100644 src/test/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifierTest.java diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..d25e006a6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# AI coding assistant guidelines: Firebase Admin Java SDK + +This document defines repository-wide expectations, code styles, and Checkstyle compliance metrics that every autonomous AI coding agent or pair programming assistant must strictly follow when introducing or modifying code in this repository. + +--- + +## 🎨 Code Style & Checkstyle Compliance + +This repository enforces standard **Google Java Style** guidelines validated through automated Maven checkstyle executions (`checkstyle.xml`). Any compilation or pull request build will fail if these constraints are violated. + +### 1. Strict 100-Character Line Limit +* **Rule:** No line of code, comments, Javadoc documentation entries, or inline string literal concatenations may exceed **100 characters** under any circumstances. +* **Remediation:** + * Break long method parameters, array initializations, and logic comparisons onto separate lines. + * Wrap long `assertThrows` statements or lambda chains across multiple contiguous lines. + * Format long log messages, exception messages, or assertion string explanations using standard string block concatenation broken across separate lines. + +### 2. Import Ordering & Grouping +* **Rule:** Imports must be grouped and arranged alphabetically to avoid validation noise. +* **Remediation:** + 1. Static imports placed first, grouped, and alphabetically arranged. + 2. Non-static imports grouped alphabetically by package tier. + 3. Avoid utilizing wildcard (`*`) imports. Every import declaration must be explicit. + +### 3. Javadoc Completeness & Formatting +* **Rule:** Every public class, package-private component interface, constructor, and public method signature must include fully formed Javadoc documentation blocks. +* **Remediation:** + * Document all arguments via `@param`, explain error flows via `@throws`, and clear return constraints via `@return`. + * Wrap documentation description text explicitly so that no single javadoc documentation line passes the 100 characters limit. + +### 4. Indentation & Spacing +* **Rule:** Standard indentation uses exactly **2 spaces** per block indentation level. **4 spaces** are used explicitly for wrapped line continuation indentation. +* **Remediation:** Never utilize tab characters. Ensure block braces follow the Google K&R opening line placement convention. diff --git a/pom.xml b/pom.xml index a642c1936..7aeebb7f9 100644 --- a/pom.xml +++ b/pom.xml @@ -446,6 +446,11 @@ httpclient5 5.3.1 + + com.nimbusds + nimbus-jose-jwt + 10.9 + diff --git a/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerification.java b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerification.java new file mode 100644 index 000000000..6960f5038 --- /dev/null +++ b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerification.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseService; +import com.google.firebase.phonenumberverification.internal.FirebasePhoneNumberVerificationTokenVerifier; + +/** + * This class is the entry point for the Firebase Phone Number Verification service. + * + *

You can get an instance of {@link FirebasePhoneNumberVerification} via {@link #getInstance()}, + * or {@link #getInstance(FirebaseApp)}. + */ +public final class FirebasePhoneNumberVerification { + private static final String SERVICE_ID = FirebasePhoneNumberVerification.class.getName(); + private final FirebasePhoneNumberVerificationTokenVerifier tokenVerifier; + + private FirebasePhoneNumberVerification(FirebaseApp app) { + this.tokenVerifier = new FirebasePhoneNumberVerificationTokenVerifier(app); + } + + /** + * Gets the {@link FirebasePhoneNumberVerification} instance for the default {@link FirebaseApp}. + * + * @return The {@link FirebasePhoneNumberVerification} instance for the default + * {@link FirebaseApp}. + */ + public static FirebasePhoneNumberVerification getInstance() { + return getInstance(FirebaseApp.getInstance()); + } + + /** + * Gets the {@link FirebasePhoneNumberVerification} instance for the specified + * {@link FirebaseApp}. + * + * @return The {@link FirebasePhoneNumberVerification} instance for the specified + * {@link FirebaseApp}. + */ + public static synchronized FirebasePhoneNumberVerification getInstance(FirebaseApp app) { + FirebasePhoneNumberVerificationService service = + ImplFirebaseTrampolines.getService(app, SERVICE_ID, + FirebasePhoneNumberVerificationService.class); + if (service == null) { + service = ImplFirebaseTrampolines.addService( + app, new FirebasePhoneNumberVerificationService(app)); + } + return service.getInstance(); + } + + /** + * Verifies a Firebase Phone Number Verification token (JWT). + * + * @param phoneNumberVerificationJwt The JWT string to verify. + * @return A verified {@link FirebasePhoneNumberVerificationToken}. + * @throws FirebasePhoneNumberVerificationException If verification fails. + */ + public FirebasePhoneNumberVerificationToken verifyToken(String phoneNumberVerificationJwt) + throws FirebasePhoneNumberVerificationException { + return this.tokenVerifier.verifyToken(phoneNumberVerificationJwt); + } + + private static class FirebasePhoneNumberVerificationService + extends FirebaseService { + FirebasePhoneNumberVerificationService(FirebaseApp app) { + super(SERVICE_ID, new FirebasePhoneNumberVerification(app)); + } + } +} diff --git a/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCode.java b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCode.java new file mode 100644 index 000000000..767f91c1e --- /dev/null +++ b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCode.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +/** + * Error codes that can be raised by the Phone Number Verification APIs. + */ +public enum FirebasePhoneNumberVerificationErrorCode { + + /** + * One or more arguments specified in the request were invalid. + */ + INVALID_ARGUMENT, + + /** + * The provided phone number verification token is invalid or malformed. + */ + INVALID_TOKEN, + + /** + * The provided phone number verification token has expired. + */ + TOKEN_EXPIRED, + + /** + * Internal error encountered during phone number verification. + */ + INTERNAL_ERROR, + + /** + * Phone number verification service is temporarily unavailable. + */ + SERVICE_ERROR, +} diff --git a/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationException.java b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationException.java new file mode 100644 index 000000000..c2e1ddfe7 --- /dev/null +++ b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; + +/** + * Generic exception related to Firebase Phone Number Verification. Check the error code and message + * for more details. + */ +public class FirebasePhoneNumberVerificationException extends FirebaseException { + + private final FirebasePhoneNumberVerificationErrorCode errorCode; + + public FirebasePhoneNumberVerificationException( + @NonNull ErrorCode errorCode, + @NonNull String message, + Throwable cause, + IncomingHttpResponse response, + FirebasePhoneNumberVerificationErrorCode phoneErrorCode) { + super(errorCode, message, cause, response); + this.errorCode = phoneErrorCode; + } + + public FirebasePhoneNumberVerificationException(FirebaseException base) { + this(base.getErrorCode(), base.getMessage(), base.getCause(), base.getHttpResponse(), null); + } + + @Nullable + public FirebasePhoneNumberVerificationErrorCode getPhoneNumberVerificationErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationToken.java b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationToken.java new file mode 100644 index 000000000..15404cdc2 --- /dev/null +++ b/src/main/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationToken.java @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a verified Firebase Phone Number Verification token. + */ +public class FirebasePhoneNumberVerificationToken { + private final Map claims; + + /** + * Create an instance of {@link FirebasePhoneNumberVerificationToken} from a map of JWT claims. + * + * @param claims A map of JWT claims. + */ + public FirebasePhoneNumberVerificationToken(Map claims) { + checkNotNull(claims, "Claims map must not be null"); + checkArgument(claims.containsKey("sub"), "Claims map must contain sub"); + this.claims = ImmutableMap.copyOf(claims); + } + + /** + * Returns the issuer identifier for the issuer of the response. + */ + public String getIssuer() { + return (String) claims.get("iss"); + } + + /** + * Returns the phone number of the user. + * This corresponds to the 'sub' claim in the JWT. + */ + public String getPhoneNumber() { + return (String) claims.get("sub"); + } + + /** + * Returns the audience for which this token is intended. + */ + public List getAudience() { + Object audience = claims.get("aud"); + if (audience instanceof String) { + return ImmutableList.of((String) audience); + } else if (audience instanceof List) { + @SuppressWarnings("unchecked") + List audienceList = (List) audience; + return ImmutableList.copyOf(audienceList); + } + return ImmutableList.of(); + } + + /** + * Returns the expiration time in seconds since the Unix epoch. + */ + public long getExpirationTime() { + Object exp = claims.get("exp"); + if (exp instanceof java.util.Date) { + return ((java.util.Date) exp).getTime() / 1000L; + } + return exp instanceof Number ? ((Number) exp).longValue() : 0L; + } + + /** + * Returns the issued-at time in seconds since the Unix epoch. + */ + public long getIssuedAt() { + Object iat = claims.get("iat"); + if (iat instanceof java.util.Date) { + return ((java.util.Date) iat).getTime() / 1000L; + } + return iat instanceof Number ? ((Number) iat).longValue() : 0L; + } + + /** + * Returns the entire map of claims. + */ + public Map getClaims() { + return claims; + } +} diff --git a/src/main/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifier.java b/src/main/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifier.java new file mode 100644 index 000000000..c51c8f9bf --- /dev/null +++ b/src/main/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifier.java @@ -0,0 +1,252 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.phonenumberverification.FirebasePhoneNumberVerificationErrorCode; +import com.google.firebase.phonenumberverification.FirebasePhoneNumberVerificationException; +import com.google.firebase.phonenumberverification.FirebasePhoneNumberVerificationToken; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.ExpiredJWTException; +import java.net.MalformedURLException; +import java.net.URI; +import java.text.ParseException; + +/** + * Internal class to verify Firebase Phone Number Verification tokens. + */ +public class FirebasePhoneNumberVerificationTokenVerifier { + private static final String FPNV_JWKS_URL = "https://fpnv.googleapis.com/v1beta/jwks"; + private static final String HEADER_TYP = "JWT"; + + private final String projectId; + private volatile DefaultJWTProcessor jwtProcessor; + + /** + * Create {@link FirebasePhoneNumberVerificationTokenVerifier} for internal purposes. + * + * @param app The {@link FirebaseApp} to get a project ID from. + */ + public FirebasePhoneNumberVerificationTokenVerifier(FirebaseApp app) { + this.projectId = getProjectId(app); + } + + /** + * Package-private constructor designed explicitly for dependency injection + * during isolated unit testing flows. + */ + FirebasePhoneNumberVerificationTokenVerifier( + String projectId, DefaultJWTProcessor jwtProcessor) { + this.projectId = projectId; + this.jwtProcessor = jwtProcessor; + } + + private DefaultJWTProcessor getJwtProcessor() { + DefaultJWTProcessor processor = this.jwtProcessor; + if (processor == null) { + synchronized (this) { + processor = this.jwtProcessor; + if (processor == null) { + processor = createJwtProcessor(); + this.jwtProcessor = processor; + } + } + } + return processor; + } + + /** + * Main method that performs token verification steps. + * + * @param token String input data + * @return {@link FirebasePhoneNumberVerificationToken} + * @throws FirebasePhoneNumberVerificationException If verification fails + */ + public FirebasePhoneNumberVerificationToken verifyToken(String token) + throws FirebasePhoneNumberVerificationException { + checkArgument(!Strings.isNullOrEmpty(token), + "Firebase Phone Number Verification token must not be null or empty"); + + try { + SignedJWT signedJwt = SignedJWT.parse(token); + verifyHeader(signedJwt.getHeader()); + + JWTClaimsSet claims = getJwtProcessor().process(signedJwt, null); + verifyClaims(claims); + + return new FirebasePhoneNumberVerificationToken(claims.getClaims()); + } catch (ParseException e) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + "Failed to parse JWT token: " + e.getMessage(), + e + ); + } catch (ExpiredJWTException e) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.TOKEN_EXPIRED, + "Firebase Phone Number Verification token has expired.", + e + ); + } catch (BadJOSEException e) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + "Check your project: " + projectId + ". " + + "Firebase Phone Number Verification token is invalid: " + + e.getMessage(), + e + ); + } catch (JOSEException e) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INTERNAL_ERROR, + "Check your project: " + projectId + ". Failed to verify " + + "Firebase Phone Number Verification token signature: " + e.getMessage(), + e + ); + } + } + + private void verifyHeader(JWSHeader header) throws FirebasePhoneNumberVerificationException { + if (!JWSAlgorithm.ES256.equals(header.getAlgorithm())) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + "Firebase Phone Number Verification token has incorrect 'algorithm'. " + + "Expected " + JWSAlgorithm.ES256.getName() + " but got " + header.getAlgorithm()); + } + if (Strings.isNullOrEmpty(header.getKeyID())) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + "Firebase Phone Number Verification token has no 'kid' claim." + ); + } + if (!JOSEObjectType.JWT.equals(header.getType())) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + "Firebase Phone Number Verification token has incorrect 'typ'. Expected " + HEADER_TYP + + " but got " + header.getType() + ); + } + } + + private void verifyClaims(JWTClaimsSet claims) throws FirebasePhoneNumberVerificationException { + checkNotNull(claims, "JWTClaimsSet claims must not be null"); + String issuer = claims.getIssuer(); + + if (Strings.isNullOrEmpty(issuer)) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + "Firebase Phone Number Verification token has no 'iss' (issuer) claim."); + } + + String expectedIssuer = "https://fpnv.googleapis.com/projects/" + this.projectId; + if (!expectedIssuer.equals(issuer)) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + "Firebase Phone Number Verification token has an incorrect 'iss' (issuer) claim."); + } + + if (claims.getAudience().isEmpty() || !claims.getAudience().contains(issuer)) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + "Invalid audience. Expected to contain: " + issuer + + " but found: " + claims.getAudience() + ); + } + + if (Strings.isNullOrEmpty(claims.getSubject())) { + throw newException( + FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + "Token has an empty 'sub' (phone number)." + ); + } + } + + private DefaultJWTProcessor createJwtProcessor() { + DefaultJWTProcessor processor = new DefaultJWTProcessor<>(); + try { + JWKSource keySource = createKeySource(); + JWSKeySelector keySelector = + new JWSVerificationKeySelector<>(JWSAlgorithm.ES256, keySource); + processor.setJWSKeySelector(keySelector); + } catch (MalformedURLException e) { + throw new IllegalStateException("Invalid JWKS URL", e); + } + return processor; + } + + protected JWKSource createKeySource() throws MalformedURLException { + return JWKSourceBuilder + .create(URI.create(FPNV_JWKS_URL).toURL()) + .retrying(true) + .build(); + } + + private String getProjectId(FirebaseApp app) { + String projectId = ImplFirebaseTrampolines.getProjectId(app); + if (Strings.isNullOrEmpty(projectId)) { + throw new IllegalArgumentException("Project ID is required in FirebaseOptions."); + } + return projectId; + } + + private FirebasePhoneNumberVerificationException newException( + FirebasePhoneNumberVerificationErrorCode errorCode, String message) { + return newException(errorCode, message, null); + } + + private FirebasePhoneNumberVerificationException newException( + FirebasePhoneNumberVerificationErrorCode errorCode, String message, Throwable cause) { + ErrorCode baseCode = ErrorCode.INTERNAL; + if (errorCode != null) { + switch (errorCode) { + case INVALID_ARGUMENT: + baseCode = ErrorCode.INVALID_ARGUMENT; + break; + case TOKEN_EXPIRED: + case INVALID_TOKEN: + baseCode = ErrorCode.UNAUTHENTICATED; + break; + case SERVICE_ERROR: + baseCode = ErrorCode.UNAVAILABLE; + break; + case INTERNAL_ERROR: + default: + baseCode = ErrorCode.INTERNAL; + break; + } + } + return new FirebasePhoneNumberVerificationException( + baseCode, message, cause, null, errorCode); + } +} diff --git a/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCodeTest.java b/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCodeTest.java new file mode 100644 index 000000000..c9f02ac23 --- /dev/null +++ b/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationErrorCodeTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class FirebasePhoneNumberVerificationErrorCodeTest { + @Test + public void testEnum() { + assertNotNull(FirebasePhoneNumberVerificationErrorCode.valueOf("INVALID_ARGUMENT")); + assertNotNull(FirebasePhoneNumberVerificationErrorCode.valueOf("INVALID_TOKEN")); + assertNotNull(FirebasePhoneNumberVerificationErrorCode.valueOf("TOKEN_EXPIRED")); + assertNotNull(FirebasePhoneNumberVerificationErrorCode.valueOf("INTERNAL_ERROR")); + assertNotNull(FirebasePhoneNumberVerificationErrorCode.valueOf("SERVICE_ERROR")); + } +} diff --git a/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTest.java b/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTest.java new file mode 100644 index 000000000..e0a2a4f85 --- /dev/null +++ b/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseProcessEnvironment; +import com.google.firebase.phonenumberverification.internal.FirebasePhoneNumberVerificationTokenVerifier; +import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestUtils; +import java.lang.reflect.Field; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class FirebasePhoneNumberVerificationTest { + private static final FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setCredentials(TestUtils.getCertCredential(ServiceAccount.OWNER.asStream())) + .build(); + + @Mock + private FirebasePhoneNumberVerificationTokenVerifier mockVerifier; + + private FirebasePhoneNumberVerification firebasePhoneNumberVerification; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + FirebaseApp.initializeApp(firebaseOptions); + firebasePhoneNumberVerification = FirebasePhoneNumberVerification.getInstance(); + + Field verifierField = FirebasePhoneNumberVerification.class.getDeclaredField("tokenVerifier"); + verifierField.setAccessible(true); + verifierField.set(firebasePhoneNumberVerification, mockVerifier); + } + + @After + public void tearDown() { + FirebaseProcessEnvironment.clearCache(); + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetInstance() { + FirebasePhoneNumberVerification instance = FirebasePhoneNumberVerification.getInstance(); + assertNotNull(instance); + assertSame(instance, FirebasePhoneNumberVerification.getInstance()); + } + + @Test + public void testGetInstanceForApp() { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testGetInstanceForApp"); + FirebasePhoneNumberVerification instance = FirebasePhoneNumberVerification.getInstance(app); + assertNotNull(instance); + assertSame(instance, FirebasePhoneNumberVerification.getInstance(app)); + } + + @Test + public void testVerifyToken_DelegatesToVerifier() + throws FirebasePhoneNumberVerificationException { + String testToken = "test.token"; + FirebasePhoneNumberVerificationToken expectedToken = + mock(FirebasePhoneNumberVerificationToken.class); + + when(mockVerifier.verifyToken(testToken)).thenReturn(expectedToken); + + FirebasePhoneNumberVerificationToken result = + firebasePhoneNumberVerification.verifyToken(testToken); + + assertEquals(expectedToken, result); + verify(mockVerifier, times(1)).verifyToken(testToken); + } + + @Test + public void testVerifyToken_PropagatesException() + throws FirebasePhoneNumberVerificationException { + String testToken = "bad.token"; + FirebasePhoneNumberVerificationException error = + new FirebasePhoneNumberVerificationException( + ErrorCode.UNAUTHENTICATED, + "Bad token", + null, + null, + FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN + ); + + when(mockVerifier.verifyToken(testToken)).thenThrow(error); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + FirebasePhoneNumberVerification.getInstance().verifyToken(testToken) + ); + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + } + + @Test + public void testVerifyToken_PropagatesException_Service_Error() + throws FirebasePhoneNumberVerificationException { + String testToken = "SERVICE_ERROR"; + FirebasePhoneNumberVerificationException error = + new FirebasePhoneNumberVerificationException( + ErrorCode.UNAVAILABLE, + "SERVICE_ERROR", + null, + null, + FirebasePhoneNumberVerificationErrorCode.SERVICE_ERROR + ); + + when(mockVerifier.verifyToken(testToken)).thenThrow(error); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + FirebasePhoneNumberVerification.getInstance().verifyToken(testToken) + ); + assertEquals(FirebasePhoneNumberVerificationErrorCode.SERVICE_ERROR, + e.getPhoneNumberVerificationErrorCode()); + } + + @Test + public void testVerifyToken_PropagatesException_Internal_Error() + throws FirebasePhoneNumberVerificationException { + String testToken = "INTERNAL"; + FirebasePhoneNumberVerificationException error = + new FirebasePhoneNumberVerificationException( + ErrorCode.INTERNAL, + "INTERNAL", + null, + null, + null + ); + + when(mockVerifier.verifyToken(testToken)).thenThrow(error); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + FirebasePhoneNumberVerification.getInstance().verifyToken(testToken) + ); + assertNull(e.getPhoneNumberVerificationErrorCode()); + } +} diff --git a/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTokenTest.java b/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTokenTest.java new file mode 100644 index 000000000..3b6ad77b8 --- /dev/null +++ b/src/test/java/com/google/firebase/phonenumberverification/FirebasePhoneNumberVerificationTokenTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseProcessEnvironment; +import com.nimbusds.jwt.JWTClaimsSet; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import org.junit.After; +import org.junit.Test; + +public class FirebasePhoneNumberVerificationTokenTest { + private static final String PROJECT_ID = "mock-project-id-1"; + private static final String ISSUER = "https://fpnv.googleapis.com/projects/" + PROJECT_ID; + private final String subject = "+15551234567"; + + @After + public void tearDown() { + FirebaseProcessEnvironment.clearCache(); + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void test_Audience_Empty() { + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .subject(subject) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + FirebasePhoneNumberVerificationToken token = + new FirebasePhoneNumberVerificationToken(claims.getClaims()); + + assertNotNull(token); + assertEquals(ImmutableList.of(), token.getAudience()); + } + + @Test + public void test_Audience_List() { + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .subject(subject) + .audience(ImmutableList.of()) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + FirebasePhoneNumberVerificationToken token = + new FirebasePhoneNumberVerificationToken(claims.getClaims()); + + assertNotNull(token); + assertEquals(ImmutableList.of(), token.getAudience()); + } + + @Test + public void test_Audience_String() { + Map claims = new HashMap<>(); + claims.put("sub", subject); + claims.put("aud", ISSUER); + + FirebasePhoneNumberVerificationToken token = new FirebasePhoneNumberVerificationToken(claims); + + assertNotNull(token); + assertEquals(ImmutableList.of(ISSUER), token.getAudience()); + } + + @Test + public void test_No_Sub() { + Map claims = new HashMap<>(); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> + new FirebasePhoneNumberVerificationToken(claims) + ); + assertTrue(e.getMessage().contains("Claims map must contain sub")); + } + + @Test + public void test_Null_Sub() { + NullPointerException e = assertThrows(NullPointerException.class, () -> + new FirebasePhoneNumberVerificationToken(null) + ); + assertEquals("Claims map must not be null", e.getMessage()); + } +} diff --git a/src/test/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifierTest.java b/src/test/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifierTest.java new file mode 100644 index 000000000..fa4ca8116 --- /dev/null +++ b/src/test/java/com/google/firebase/phonenumberverification/internal/FirebasePhoneNumberVerificationTokenVerifierTest.java @@ -0,0 +1,456 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.phonenumberverification.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseProcessEnvironment; +import com.google.firebase.phonenumberverification.FirebasePhoneNumberVerificationErrorCode; +import com.google.firebase.phonenumberverification.FirebasePhoneNumberVerificationException; +import com.google.firebase.phonenumberverification.FirebasePhoneNumberVerificationToken; +import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestUtils; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.ExpiredJWTException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class FirebasePhoneNumberVerificationTokenVerifierTest { + private static final String PROJECT_ID = "mock-project-id-2"; + private static final FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setProjectId(PROJECT_ID) + .setCredentials(TestUtils.getCertCredential(ServiceAccount.OWNER.asStream())) + .build(); + private static final String ISSUER = "https://fpnv.googleapis.com/projects/" + PROJECT_ID; + private static final String[] AUD = new String[]{ + ISSUER, + "https://google.com/projects/" + }; + + @Mock + private DefaultJWTProcessor mockJwtProcessor; + + private FirebasePhoneNumberVerificationTokenVerifier verifier; + private KeyPair rsaKeyPair; + private ECKey ecKey; + private JWSHeader header; + private JWTClaimsSet claims; + private final String subject = "+15551234567"; + private final Date issueTime = new Date(); + private final Date expirationTime = new Date(System.currentTimeMillis() + 10000); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(2048); + rsaKeyPair = gen.generateKeyPair(); + + ecKey = new ECKeyGenerator(Curve.P_256).keyID("ec-key-id").generate(); + + FirebaseApp firebaseApp = FirebaseApp.initializeApp(firebaseOptions); + verifier = new FirebasePhoneNumberVerificationTokenVerifier(firebaseApp); + + Field processorField = FirebasePhoneNumberVerificationTokenVerifier.class + .getDeclaredField("jwtProcessor"); + processorField.setAccessible(true); + processorField.set(verifier, mockJwtProcessor); + + header = new JWSHeader.Builder(JWSAlgorithm.ES256) + .keyID(ecKey.getKeyID()) + .type(JOSEObjectType.JWT) + .build(); + + claims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .audience(Arrays.asList(AUD)) + .subject(subject) + .issueTime(issueTime) + .expirationTime(expirationTime) + .build(); + } + + @After + public void tearDown() { + FirebaseProcessEnvironment.clearCache(); + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + private String createToken(JWSHeader header, JWTClaimsSet claims) throws Exception { + SignedJWT jwt = new SignedJWT(header, claims); + + if (JWSAlgorithm.RS256.equals(header.getAlgorithm())) { + jwt.sign(new RSASSASigner(rsaKeyPair.getPrivate())); + } else if (JWSAlgorithm.HS256.equals(header.getAlgorithm())) { + jwt.sign(new MACSigner("12345678901234567890123456789012")); + } else if (JWSAlgorithm.ES256.equals(header.getAlgorithm())) { + jwt.sign(new ECDSASigner(ecKey.toECPrivateKey())); + } + + return jwt.serialize(); + } + + @Test + public void testVerifyToken_NullOrEmptyToken() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> verifier.verifyToken("")); + assertTrue(e.getMessage().contains( + "Firebase Phone Number Verification token must not be null")); + } + + @Test + public void testVerifyToken_Success() throws Exception { + String tokenString = createToken(header, claims); + + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(claims); + + FirebasePhoneNumberVerificationToken result = verifier.verifyToken(tokenString); + + assertNotNull(result); + assertEquals(subject, result.getPhoneNumber()); + assertEquals(issueTime.getTime() / 1000L, result.getIssuedAt()); + assertEquals(expirationTime.getTime() / 1000L, result.getExpirationTime()); + assertEquals(Arrays.asList(AUD), result.getAudience()); + assertEquals(ISSUER, result.getIssuer()); + assertEquals(ISSUER, result.getClaims().get("iss")); + } + + @Test + public void testVerifyToken_Header_WrongAlgorithm() throws Exception { + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build(); + JWTClaimsSet claims = new JWTClaimsSet.Builder().build(); + + String tokenString = createToken(header, claims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("algorithm")); + } + + @Test + public void testVerifyToken_Header_WrongTyp() throws Exception { + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256) + .keyID(ecKey.getKeyID()) + .type(JOSEObjectType.JOSE) + .build(); + JWTClaimsSet claims = new JWTClaimsSet.Builder().build(); + + String tokenString = createToken(header, claims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("has incorrect 'typ'")); + } + + @Test + public void testVerifyToken_Header_MissingKeyId() throws Exception { + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).build(); + JWTClaimsSet claims = new JWTClaimsSet.Builder().build(); + + String tokenString = createToken(header, claims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains( + "Firebase Phone Number Verification token has no 'kid' claim")); + } + + @Test + public void testVerifyToken_Claims_Null() throws Exception { + JWTClaimsSet noSubClaims = new JWTClaimsSet.Builder().build(); + + String tokenString = createToken(header, noSubClaims); + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(null); + + NullPointerException e = assertThrows(NullPointerException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertTrue(e.getMessage().contains("JWTClaimsSet claims must not be null")); + } + + @Test + public void testVerifyToken_Claims_NoIssuer() throws Exception { + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .audience(ISSUER) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + String tokenString = createToken(header, claims); + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(claims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_ARGUMENT, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains( + "Firebase Phone Number Verification token has no 'iss' (issuer) claim.")); + } + + @Test + public void testVerifyToken_Claims_Expired() throws Exception { + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .audience(ISSUER) + .subject("+1555") + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + String tokenString = createToken(header, claims); + ExpiredJWTException error = new ExpiredJWTException("Bad token"); + + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenThrow(error); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.TOKEN_EXPIRED, + e.getPhoneNumberVerificationErrorCode()); + } + + @Test + public void testVerifyToken_Claims_WrongAudience() throws Exception { + JWTClaimsSet badClaims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .audience("https://wrong-audience.com") + .subject(subject) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + String tokenString = createToken(header, badClaims); + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(badClaims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("Invalid audience.")); + } + + @Test + public void testVerifyToken_Claims_EmptyAudience() throws Exception { + JWTClaimsSet badClaims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .audience(Collections.emptyList()) + .subject(subject) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + String tokenString = createToken(header, badClaims); + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(badClaims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("Invalid audience. Expected to contain: ")); + } + + @Test + public void testVerifyToken_Claims_NoSubject() throws Exception { + JWTClaimsSet noSubClaims = new JWTClaimsSet.Builder() + .issuer(ISSUER) + .audience(ISSUER) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + String tokenString = createToken(header, noSubClaims); + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(noSubClaims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("Token has an empty 'sub' (phone number)")); + } + + @Test + public void testVerifyToken_ParseException() { + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(" ") + ); + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("Failed to parse JWT token")); + } + + @Test + public void testVerifyToken_BadJOSEException() throws Exception { + String tokenString = createToken(header, claims); + String errorMessage = "BadJOSEException"; + BadJOSEException error = new BadJOSEException(errorMessage); + + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenThrow(error); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("Firebase Phone Number Verification token is invalid:")); + } + + @Test + public void testVerifyToken_JOSEException() throws Exception { + String tokenString = createToken(header, claims); + String errorMessage = "JOSEException"; + JOSEException error = new JOSEException(errorMessage); + + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenThrow(error); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INTERNAL_ERROR, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains( + "Failed to verify Firebase Phone Number Verification token signature:")); + } + + @Test + public void testVerifierWithoutProjectId() { + FirebaseOptions localFirebaseOptions = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.create(null)) + .build(); + + FirebaseApp firebaseApp = FirebaseApp.initializeApp(localFirebaseOptions, "second"); + + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> + new FirebasePhoneNumberVerificationTokenVerifier(firebaseApp) + ); + + assertEquals("Project ID is required in FirebaseOptions.", e.getMessage()); + } + + @Test + public void testCreateJwtProcessor_HandlesException() throws Exception { + FirebaseApp firebaseApp = FirebaseApp.initializeApp(firebaseOptions, "third"); + FirebasePhoneNumberVerificationTokenVerifier original = + new FirebasePhoneNumberVerificationTokenVerifier(firebaseApp); + FirebasePhoneNumberVerificationTokenVerifier spyClass = spy(original); + + doThrow(new MalformedURLException("Simulated bad URL")) + .when(spyClass).createKeySource(); + + Method method = FirebasePhoneNumberVerificationTokenVerifier.class + .getDeclaredMethod("createJwtProcessor"); + method.setAccessible(true); + + try { + method.invoke(spyClass); + } catch (Exception e) { + Throwable cause = e.getCause(); + assertEquals(IllegalStateException.class, cause.getClass()); + assertEquals("Invalid JWKS URL", cause.getMessage()); + assertTrue(cause.getCause() instanceof MalformedURLException); + } + } + + @Test + public void testVerifyToken_Claims_InvalidIssuerProject() throws Exception { + JWTClaimsSet badIssuerClaims = new JWTClaimsSet.Builder() + .issuer("https://fpnv.googleapis.com/projects/attacker-project-id") + .audience("https://fpnv.googleapis.com/projects/attacker-project-id") + .subject(subject) + .expirationTime(new Date(System.currentTimeMillis() + 10000)) + .build(); + + String tokenString = createToken(header, badIssuerClaims); + when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(badIssuerClaims); + + FirebasePhoneNumberVerificationException e = + assertThrows(FirebasePhoneNumberVerificationException.class, () -> + verifier.verifyToken(tokenString) + ); + + assertEquals(FirebasePhoneNumberVerificationErrorCode.INVALID_TOKEN, + e.getPhoneNumberVerificationErrorCode()); + assertTrue(e.getMessage().contains("incorrect 'iss' (issuer) claim")); + } +} From 62c5bb6e913730e3d365d6f1776c682b66bce0e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 12:55:44 -0400 Subject: [PATCH 202/219] chore(deps): Bump io.netty:netty-codec-http (#1204) Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.2.10.Final to 4.2.13.Final. - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.10.Final...netty-4.2.13.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.13.Final dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7aeebb7f9..d27d01a77 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.10.Final + 4.2.13.Final From e471ee1fe449888b8d270cba5f1222ad3804781f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 13:02:53 -0400 Subject: [PATCH 203/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.76.0 to 26.77.0 (#1193) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.76.0 to 26.77.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.76.0...v26.77.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.77.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d27d01a77..925efbec3 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.76.0 + 26.77.0 pom import From 8e92e878806a8a9696c9587312056e52c4cf528a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 13:14:20 -0400 Subject: [PATCH 204/219] chore(deps-dev): Bump org.apache.maven.plugins:maven-surefire-plugin (#1190) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.4 to 3.5.5. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 925efbec3..e49d89567 100644 --- a/pom.xml +++ b/pom.xml @@ -273,7 +273,7 @@ maven-surefire-plugin - 3.5.4 + 3.5.5 ${skipUTs} From b465586481f912582098a56319412f352744cbd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 13:29:00 -0400 Subject: [PATCH 205/219] chore(deps-dev): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1188) Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.4 to 3.5.5. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e49d89567..4b323a8ac 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,7 @@ maven-failsafe-plugin - 3.5.4 + 3.5.5 From 533bc3d39c27a6c0cfa316bc996adf27f652560b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 14 May 2026 16:35:33 -0400 Subject: [PATCH 206/219] [chore] Release 9.9.0 (#1206) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4b323a8ac..0807bd96a 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.8.0 + 9.9.0 jar firebase-admin From d86e023f68113642bfeacd903ba10acd09fc1508 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 09:39:24 -0400 Subject: [PATCH 207/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.77.0 to 26.82.0 (#1209) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.77.0 to 26.82.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.77.0...v26.82.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.82.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0807bd96a..f165f86b4 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.77.0 + 26.82.0 pom import From 0ec98fac63c0103006311d3ecdce498d8c3f9d15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 09:43:23 -0400 Subject: [PATCH 208/219] chore(deps): Bump org.slf4j:slf4j-api from 2.0.17 to 2.0.18 (#1208) Bumps org.slf4j:slf4j-api from 2.0.17 to 2.0.18. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-version: 2.0.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f165f86b4..5e0547113 100644 --- a/pom.xml +++ b/pom.xml @@ -424,7 +424,7 @@ org.slf4j slf4j-api - 2.0.17 + 2.0.18 io.netty From d5c6cb91edce90a7b4187791a8c6f90a437624c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:29:36 -0400 Subject: [PATCH 209/219] chore(deps): Bump netty.version from 4.2.13.Final to 4.2.14.Final (#1213) Bumps `netty.version` from 4.2.13.Final to 4.2.14.Final. Updates `io.netty:netty-codec-http` from 4.2.13.Final to 4.2.14.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.13.Final...netty-4.2.14.Final) Updates `io.netty:netty-handler` from 4.2.13.Final to 4.2.14.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.13.Final...netty-4.2.14.Final) Updates `io.netty:netty-transport` from 4.2.13.Final to 4.2.14.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.13.Final...netty-4.2.14.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.14.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.14.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.14.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5e0547113..cf17319cf 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.13.Final + 4.2.14.Final From c2be37e8df2e5801d2c8ca61f47699aaa0e49ace Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 12:53:25 -0400 Subject: [PATCH 210/219] chore(deps): Bump com.google.cloud:libraries-bom from 26.82.0 to 26.83.0 (#1212) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.82.0 to 26.83.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.82.0...v26.83.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.83.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cf17319cf..a161d337d 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.82.0 + 26.83.0 pom import From c16536694d4beedede4d26cabf51d8f9eebc6849 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:38:30 -0400 Subject: [PATCH 211/219] chore(deps): Bump netty.version from 4.2.14.Final to 4.2.15.Final (#1218) Bumps `netty.version` from 4.2.14.Final to 4.2.15.Final. Updates `io.netty:netty-codec-http` from 4.2.14.Final to 4.2.15.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.14.Final...netty-4.2.15.Final) Updates `io.netty:netty-handler` from 4.2.14.Final to 4.2.15.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.14.Final...netty-4.2.15.Final) Updates `io.netty:netty-transport` from 4.2.14.Final to 4.2.15.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.14.Final...netty-4.2.15.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.15.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.15.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.15.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a161d337d..1a01951df 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.14.Final + 4.2.15.Final From ba7917da119d538c95300e780d12ca50e9b9ddd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:43:41 -0400 Subject: [PATCH 212/219] chore(deps-dev): Bump org.apache.maven.plugins:maven-surefire-plugin (#1217) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.5 to 3.5.6. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.5...surefire-3.5.6) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1a01951df..c6e2674f2 100644 --- a/pom.xml +++ b/pom.xml @@ -273,7 +273,7 @@ maven-surefire-plugin - 3.5.5 + 3.5.6 ${skipUTs} From 5ac228cdfa1fc9dc621cb5df1ebfc455227ef2fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:50:18 -0400 Subject: [PATCH 213/219] chore(deps-dev): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1216) Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.5 to 3.5.6. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.5...surefire-3.5.6) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c6e2674f2..99cf7c52d 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,7 @@ maven-failsafe-plugin - 3.5.5 + 3.5.6 From 3d7434c8f3746068960f9ad2c4018d66ec322ed5 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:40:04 -0400 Subject: [PATCH 214/219] feat(fcm): Enable `fid` and deprecate `token` for Send API (#1211) * Enable fid and deprecate token for Message Class * Add fid supported methods and deprecate the token methods for the MulticastMessage Class * Add integration tests and update all public multicast send methods * Update docstring * Update integration tests error code for invalid fid target --- .../firebase/messaging/FirebaseMessaging.java | 44 ++++---- .../google/firebase/messaging/Message.java | 37 ++++++- .../firebase/messaging/MulticastMessage.java | 97 +++++++++++++---- .../messaging/FirebaseMessagingIT.java | 88 +++++++++++++++ .../firebase/messaging/MessageTest.java | 22 ++++ .../messaging/MulticastMessageTest.java | 101 ++++++++++++++++++ 6 files changed, 352 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index bb7b8208f..0e9831588 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -265,12 +265,14 @@ protected SendResponse execute() { } /** - * Sends the given multicast message to all the FCM registration tokens specified in it. + * Sends the given multicast message to all the FCM registration tokens and/or FIDs + * specified in it. * *

This method uses the {@link #sendEach(List)} API under the hood to send the given * message to all the target recipients. The list of responses obtained by calling * {@link BatchResponse#getResponses()} on the return value is in the same order as the - * tokens in the {@link MulticastMessage}. + * tokens and/or FIDs in the {@link MulticastMessage}. If both tokens and FIDs are + * provided, tokens are processed first, followed by FIDs. * * @param message A non-null {@link MulticastMessage} * @return A {@link BatchResponse} indicating the result of the operation. @@ -285,17 +287,19 @@ public BatchResponse sendEachForMulticast( } /** - * Sends the given multicast message to all the FCM registration tokens specified in it. + * Sends the given multicast message to all the FCM registration tokens and/or FIDs + * specified in it. * - *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead - * FCM performs all the necessary validations, and emulates the send operation. The {@code dryRun} - * option is useful for determining whether an FCM registration has been deleted. But it cannot be - * used to validate APNs tokens. + *

If the {@code dryRun} option is set to true, the message will not be actually sent. + * Instead FCM performs all the necessary validations, and emulates the send operation. + * The {@code dryRun} option is useful for determining whether an FCM registration has + * been deleted. But it cannot be used to validate APNs tokens. * *

This method uses the {@link #sendEach(List)} API under the hood to send the given * message to all the target recipients. The list of responses obtained by calling * {@link BatchResponse#getResponses()} on the return value is in the same order as the - * tokens in the {@link MulticastMessage}. + * tokens and/or FIDs in the {@link MulticastMessage}. If both tokens and FIDs are + * provided, tokens are processed first, followed by FIDs. * * @param message A non-null {@link MulticastMessage}. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. @@ -417,12 +421,14 @@ public ApiFuture sendAllAsync( } /** - * Sends the given multicast message to all the FCM registration tokens specified in it. + * Sends the given multicast message to all the FCM registration tokens and/or FIDs + * specified in it. * *

This method uses the {@link #sendAll(List)} API under the hood to send the given * message to all the target recipients. The responses list obtained by calling - * {@link BatchResponse#getResponses()} on the return value corresponds to the order of tokens - * in the {@link MulticastMessage}. + * {@link BatchResponse#getResponses()} on the return value corresponds to the order of + * tokens and/or FIDs in the {@link MulticastMessage}. If both tokens and FIDs are + * provided, tokens are processed first, followed by FIDs. * * @param message A non-null {@link MulticastMessage} * @return A {@link BatchResponse} indicating the result of the operation. @@ -439,17 +445,19 @@ public BatchResponse sendMulticast( } /** - * Sends the given multicast message to all the FCM registration tokens specified in it. + * Sends the given multicast message to all the FCM registration tokens and/or FIDs + * specified in it. * - *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead - * FCM performs all the necessary validations, and emulates the send operation. The {@code dryRun} - * option is useful for determining whether an FCM registration has been deleted. But it cannot be - * used to validate APNs tokens. + *

If the {@code dryRun} option is set to true, the message will not be actually sent. + * Instead FCM performs all the necessary validations, and emulates the send operation. + * The {@code dryRun} option is useful for determining whether an FCM registration has + * been deleted. But it cannot be used to validate APNs tokens. * *

This method uses the {@link #sendAll(List)} API under the hood to send the given * message to all the target recipients. The responses list obtained by calling - * {@link BatchResponse#getResponses()} on the return value corresponds to the order of tokens - * in the {@link MulticastMessage}. + * {@link BatchResponse#getResponses()} on the return value corresponds to the order of + * tokens and/or FIDs in the {@link MulticastMessage}. If both tokens and FIDs are + * provided, tokens are processed first, followed by FIDs. * * @param message A non-null {@link MulticastMessage}. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index 1514fce3d..da790fa7f 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -54,9 +54,16 @@ public class Message { @Key("apns") private final ApnsConfig apnsConfig; + /** + * @deprecated Use {@link #fid} instead. + */ + @Deprecated @Key("token") private final String token; + @Key("fid") + private final String fid; + @Key("topic") private final String topic; @@ -74,11 +81,14 @@ private Message(Builder builder) { this.apnsConfig = builder.apnsConfig; int count = Booleans.countTrue( !Strings.isNullOrEmpty(builder.token), + !Strings.isNullOrEmpty(builder.fid), !Strings.isNullOrEmpty(builder.topic), !Strings.isNullOrEmpty(builder.condition) ); - checkArgument(count == 1, "Exactly one of token, topic or condition must be specified"); + checkArgument(count == 1, + "Exactly one of token, fid, topic or condition must be specified"); this.token = builder.token; + this.fid = builder.fid; this.topic = stripPrefix(builder.topic); this.condition = builder.condition; this.fcmOptions = builder.fcmOptions; @@ -109,11 +119,20 @@ ApnsConfig getApnsConfig() { return apnsConfig; } + /** + * @deprecated Use {@link #getFid()} instead. + */ + @Deprecated @VisibleForTesting String getToken() { return token; } + @VisibleForTesting + String getFid() { + return fid; + } + @VisibleForTesting String getTopic() { return topic; @@ -166,7 +185,9 @@ public static class Builder { private AndroidConfig androidConfig; private WebpushConfig webpushConfig; private ApnsConfig apnsConfig; + @Deprecated private String token; + private String fid; private String topic; private String condition; private FcmOptions fcmOptions; @@ -222,12 +243,26 @@ public Builder setApnsConfig(ApnsConfig apnsConfig) { * * @param token A valid device registration token. * @return This builder. + * @deprecated Use {@link #setFid(String)} instead. */ + @Deprecated public Builder setToken(String token) { this.token = token; return this; } + /** + * Sets the Firebase Installation ID (FID) of the app instance to which the message + * should be sent. + * + * @param fid A valid Firebase Installation ID. + * @return This builder. + */ + public Builder setFid(String fid) { + this.fid = fid; + return this; + } + /** * Sets the name of the FCM topic to which the message should be sent. Topic names may * contain the {@code /topics/} prefix. diff --git a/src/main/java/com/google/firebase/messaging/MulticastMessage.java b/src/main/java/com/google/firebase/messaging/MulticastMessage.java index cc43b187b..a880a88f2 100644 --- a/src/main/java/com/google/firebase/messaging/MulticastMessage.java +++ b/src/main/java/com/google/firebase/messaging/MulticastMessage.java @@ -30,22 +30,26 @@ /** * Represents a message that can be sent to multiple devices via Firebase Cloud Messaging (FCM). - * Contains payload information as well as the list of device registration tokens to which the - * message should be sent. A single {@code MulticastMessage} may contain up to 500 registration - * tokens. + * Contains payload information as well as the list of device registration tokens and/or + * Firebase Installation IDs (FIDs) to which the message should be sent. A single + * {@code MulticastMessage} may contain up to 500 registration tokens and FIDs combined. * *

Instances of this class are thread-safe and immutable. Use {@link MulticastMessage.Builder} * to create new instances. See {@link FirebaseMessaging#sendMulticast(MulticastMessage)} for * details on how to send the message to FCM for multicast delivery. * - *

This class and the associated Builder retain the order of tokens. Therefore the order of - * the responses list obtained by calling {@link BatchResponse#getResponses()} on the return value - * of {@link FirebaseMessaging#sendMulticast(MulticastMessage)} corresponds to the order in which - * tokens were added to the {@link MulticastMessage.Builder}. + *

This class and the associated Builder retain the order of tokens and FIDs. Therefore + * the order of the responses list obtained by calling {@link BatchResponse#getResponses()} + * on the return value of {@link FirebaseMessaging#sendMulticast(MulticastMessage)} + * corresponds to the order in which targets were added to the + * {@link MulticastMessage.Builder}. If both tokens and FIDs are provided, tokens are + * processed first, followed by FIDs. */ public class MulticastMessage { + @Deprecated private final List tokens; + private final List fids; private final Map data; private final Notification notification; private final AndroidConfig androidConfig; @@ -55,11 +59,18 @@ public class MulticastMessage { private MulticastMessage(Builder builder) { this.tokens = builder.tokens.build(); - checkArgument(!this.tokens.isEmpty(), "at least one token must be specified"); - checkArgument(this.tokens.size() <= 500, "no more than 500 tokens can be specified"); + this.fids = builder.fids.build(); + int tokensSize = this.tokens.size(); + int fidsSize = this.fids.size(); + checkArgument(tokensSize + fidsSize > 0, "at least one token or fid must be specified"); + checkArgument(tokensSize + fidsSize <= 500, + "no more than 500 tokens and fids combined can be specified"); for (String token : this.tokens) { checkArgument(!Strings.isNullOrEmpty(token), "none of the tokens can be null or empty"); } + for (String fid : this.fids) { + checkArgument(!Strings.isNullOrEmpty(fid), "none of the fids can be null or empty"); + } this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; this.androidConfig = builder.androidConfig; @@ -69,6 +80,26 @@ private MulticastMessage(Builder builder) { } List getMessageList() { + ImmutableList.Builder messages = ImmutableList.builder(); + + if (!this.tokens.isEmpty()) { + Message.Builder tokenBuilder = getMetadataBuilder(); + for (String token : this.tokens) { + messages.add(tokenBuilder.setToken(token).build()); + } + } + + if (!this.fids.isEmpty()) { + Message.Builder fidBuilder = getMetadataBuilder(); + for (String fid : this.fids) { + messages.add(fidBuilder.setFid(fid).build()); + } + } + + return messages.build(); + } + + private Message.Builder getMetadataBuilder() { Message.Builder builder = Message.builder() .setNotification(this.notification) .setAndroidConfig(this.androidConfig) @@ -78,11 +109,7 @@ List getMessageList() { if (this.data != null) { builder.putAllData(this.data); } - ImmutableList.Builder messages = ImmutableList.builder(); - for (String token : this.tokens) { - messages.add(builder.setToken(token).build()); - } - return messages.build(); + return builder; } /** @@ -96,7 +123,9 @@ public static Builder builder() { public static class Builder { + @Deprecated private final ImmutableList.Builder tokens = ImmutableList.builder(); + private final ImmutableList.Builder fids = ImmutableList.builder(); private final Map data = new HashMap<>(); private Notification notification; private AndroidConfig androidConfig; @@ -107,29 +136,61 @@ public static class Builder { private Builder() {} /** - * Adds a token to which the message should be sent. Up to 500 tokens can be specified on - * a single instance of {@link MulticastMessage}. + * Adds a token to which the message should be sent. Up to 500 tokens + * and FIDs combined can be specified on a single instance of + * {@link MulticastMessage}. * * @param token A non-null, non-empty Firebase device registration token. * @return This builder. + * @deprecated Use {@link #addFid(String)} instead. */ + @Deprecated public Builder addToken(@NonNull String token) { this.tokens.add(token); return this; } /** - * Adds a collection of tokens to which the message should be sent. Up to 500 tokens can be - * specified on a single instance of {@link MulticastMessage}. + * Adds a Firebase Installation ID (FID) to which the message should be sent. + * Up to 500 tokens and FIDs combined can be specified on a single instance + * of {@link MulticastMessage}. + * + * @param fid A non-null, non-empty Firebase Installation ID. + * @return This builder. + */ + public Builder addFid(@NonNull String fid) { + this.fids.add(fid); + return this; + } + + /** + * Adds a collection of tokens to which the message should be sent. Up to 500 + * tokens and FIDs combined can be specified on a single instance of + * {@link MulticastMessage}. * * @param tokens Collection of Firebase device registration tokens. * @return This builder. + * @deprecated Use {@link #addAllFids(Collection)} instead. */ + @Deprecated public Builder addAllTokens(@NonNull Collection tokens) { this.tokens.addAll(tokens); return this; } + /** + * Adds a collection of Firebase Installation IDs (FIDs) to which the message + * should be sent. Up to 500 tokens and FIDs combined can be specified on a + * single instance of {@link MulticastMessage}. + * + * @param fids Collection of Firebase Installation IDs. + * @return This builder. + */ + public Builder addAllFids(@NonNull Collection fids) { + this.fids.addAll(fids); + return this; + } + /** * Sets the notification information to be included in the message. * diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index e084b8c29..85667f799 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.http.HttpResponseException; import com.google.common.collect.ImmutableList; @@ -97,6 +98,28 @@ public void testSendError() throws InterruptedException { } } + @Test + public void testSendFidError() throws InterruptedException { + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .setFid("not-a-fid") + .build(); + try { + messaging.sendAsync(message, true).get(); + fail("No exception thrown for invalid FID"); + } catch (ExecutionException e) { + FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, cause.getErrorCode()); + assertEquals(MessagingErrorCode.UNREGISTERED, cause.getMessagingErrorCode()); + assertNotNull(cause.getHttpResponse()); + assertTrue(cause.getCause() instanceof HttpResponseException); + } + } + @Test public void testSendEach() throws Exception { List messages = new ArrayList<>(); @@ -194,6 +217,71 @@ public void testSendEachForMulticast() throws Exception { } } + @Test + public void testSendEachForMulticastFidsError() throws Exception { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .addFid("not-a-fid") + .addFid("also-not-a-fid") + .build(); + + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast( + multicastMessage, true); + + assertEquals(0, response.getSuccessCount()); + assertEquals(2, response.getFailureCount()); + assertEquals(2, response.getResponses().size()); + for (SendResponse sendResponse : response.getResponses()) { + assertFalse(sendResponse.isSuccessful()); + assertNull(sendResponse.getMessageId()); + assertNotNull(sendResponse.getException()); + assertEquals(ErrorCode.NOT_FOUND, + sendResponse.getException().getErrorCode()); + assertEquals(MessagingErrorCode.UNREGISTERED, + sendResponse.getException().getMessagingErrorCode()); + } + } + + @Test + public void testSendEachForMulticastMixedError() throws Exception { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .addToken("not-a-token") + .addFid("not-a-fid") + .build(); + + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast( + multicastMessage, true); + + assertEquals(0, response.getSuccessCount()); + assertEquals(2, response.getFailureCount()); + assertEquals(2, response.getResponses().size()); + + SendResponse response1 = response.getResponses().get(0); + assertFalse(response1.isSuccessful()); + assertNull(response1.getMessageId()); + assertNotNull(response1.getException()); + assertEquals(ErrorCode.INVALID_ARGUMENT, response1.getException().getErrorCode()); + assertEquals( + MessagingErrorCode.INVALID_ARGUMENT, + response1.getException().getMessagingErrorCode()); + + SendResponse response2 = response.getResponses().get(1); + assertFalse(response2.isSuccessful()); + assertNull(response2.getMessageId()); + assertNotNull(response2.getException()); + assertEquals(ErrorCode.NOT_FOUND, response2.getException().getErrorCode()); + assertEquals( + MessagingErrorCode.UNREGISTERED, + response2.getException().getMessagingErrorCode()); + } + @Test public void testSubscribe() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 94ca4fa4a..0bca10df1 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -54,6 +54,28 @@ public void testEmptyMessage() throws IOException { Message.builder().setCondition("'foo' in topics").build()); assertJsonEquals(ImmutableMap.of("token", "test-token"), Message.builder().setToken("test-token").build()); + assertJsonEquals(ImmutableMap.of("fid", "test-fid"), + Message.builder().setFid("test-fid").build()); + } + + @Test + public void testMultipleTargets() { + List builders = ImmutableList.of( + Message.builder().setToken("token").setFid("fid"), + Message.builder().setToken("token").setTopic("topic"), + Message.builder().setFid("fid").setCondition("cond"), + Message.builder().setTopic("topic").setCondition("cond"), + Message.builder().setToken("token").setFid("fid").setTopic("topic").setCondition("cond") + ); + for (int i = 0; i < builders.size(); i++) { + try { + builders.get(i).build(); + fail("No error thrown for multiple targets: " + i); + } catch (IllegalArgumentException expected) { + assertEquals("Exactly one of token, fid, topic or condition must be specified", + expected.getMessage()); + } + } } @Test diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index aa0dae10c..481ccccc9 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -86,6 +86,21 @@ public void testTooManyTokens() { } } + @Test + public void testTooManyFids() { + MulticastMessage.Builder builder = MulticastMessage.builder(); + for (int i = 0; i < 501; i++) { + builder.addFid("fid" + i); + } + try { + builder.build(); + fail("No error thrown for more than 500 fids"); + } catch (IllegalArgumentException expected) { + assertEquals("no more than 500 tokens and fids combined can be specified", + expected.getMessage()); + } + } + @Test(expected = NullPointerException.class) public void testNullToken() { MulticastMessage.builder().addToken(null).build(); @@ -96,6 +111,92 @@ public void testEmptyToken() { MulticastMessage.builder().addToken("").build(); } + @Test + public void testMulticastMessageFids() { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setAndroidConfig(ANDROID) + .setApnsConfig(APNS) + .setWebpushConfig(WEBPUSH) + .setNotification(NOTIFICATION) + .setFcmOptions(FCM_OPTIONS) + .putData("key1", "value1") + .putAllData(ImmutableMap.of("key2", "value2")) + .addFid("fid1") + .addAllFids(ImmutableList.of("fid2", "fid3")) + .build(); + + List messages = multicastMessage.getMessageList(); + + assertEquals(3, messages.size()); + for (int i = 0; i < 3; i++) { + Message message = messages.get(i); + assertMessageFid(message, "fid" + (i + 1)); + } + } + + @Test + public void testMulticastMessageMixed() { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setAndroidConfig(ANDROID) + .setApnsConfig(APNS) + .setWebpushConfig(WEBPUSH) + .setNotification(NOTIFICATION) + .setFcmOptions(FCM_OPTIONS) + .putData("key1", "value1") + .putAllData(ImmutableMap.of("key2", "value2")) + .addToken("token1") + .addFid("fid1") + .addToken("token2") + .addFid("fid2") + .build(); + + List messages = multicastMessage.getMessageList(); + + assertEquals(4, messages.size()); + assertMessage(messages.get(0), "token1"); + assertMessage(messages.get(1), "token2"); + assertMessageFid(messages.get(2), "fid1"); + assertMessageFid(messages.get(3), "fid2"); + } + + @Test + public void testTooManyTargetsCombined() { + MulticastMessage.Builder builder = MulticastMessage.builder(); + for (int i = 0; i < 250; i++) { + builder.addToken("token" + i); + } + for (int i = 0; i < 251; i++) { + builder.addFid("fid" + i); + } + try { + builder.build(); + fail("No error thrown for more than 500 combined targets"); + } catch (IllegalArgumentException expected) { + assertEquals("no more than 500 tokens and fids combined can be specified", + expected.getMessage()); + } + } + + @Test(expected = NullPointerException.class) + public void testNullFid() { + MulticastMessage.builder().addFid(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyFid() { + MulticastMessage.builder().addFid("").build(); + } + + private void assertMessageFid(Message message, String expectedFid) { + assertSame(ANDROID, message.getAndroidConfig()); + assertSame(APNS, message.getApnsConfig()); + assertSame(WEBPUSH, message.getWebpushConfig()); + assertSame(NOTIFICATION, message.getNotification()); + assertSame(FCM_OPTIONS, message.getFcmOptions()); + assertEquals(ImmutableMap.of("key1", "value1", "key2", "value2"), message.getData()); + assertEquals(expectedFid, message.getFid()); + } + private void assertMessage(Message message, String expectedToken) { assertSame(ANDROID, message.getAndroidConfig()); assertSame(APNS, message.getApnsConfig()); From e17fa586088867c300ad6dc232a54d86e757fcce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:36:47 -0400 Subject: [PATCH 215/219] chore(deps): Bump com.nimbusds:nimbus-jose-jwt from 10.9 to 10.9.1 (#1220) Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 10.9 to 10.9.1. - [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt) - [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.9.1..10.9) --- updated-dependencies: - dependency-name: com.nimbusds:nimbus-jose-jwt dependency-version: 10.9.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 99cf7c52d..a86ce31af 100644 --- a/pom.xml +++ b/pom.xml @@ -449,7 +449,7 @@ com.nimbusds nimbus-jose-jwt - 10.9 + 10.9.1 From 4d48b282748e7c94326964ed8aef421c2e051426 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:39:44 -0400 Subject: [PATCH 216/219] chore(deps-dev): Bump org.sonatype.central:central-publishing-maven-plugin (#1221) Bumps [org.sonatype.central:central-publishing-maven-plugin](https://github.com/sonatype/central-publishing-maven-plugin) from 0.10.0 to 0.11.0. - [Commits](https://github.com/sonatype/central-publishing-maven-plugin/commits) --- updated-dependencies: - dependency-name: org.sonatype.central:central-publishing-maven-plugin dependency-version: 0.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a86ce31af..ba9895367 100644 --- a/pom.xml +++ b/pom.xml @@ -345,7 +345,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.10.0 + 0.11.0 true central From 7c0d9264adf3316a16d7a140488be47ea9fe340b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:23:46 -0400 Subject: [PATCH 217/219] chore(deps-dev): Bump org.jacoco:jacoco-maven-plugin (#1219) Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.14 to 0.8.15. - [Release notes](https://github.com/jacoco/jacoco/releases) - [Commits](https://github.com/jacoco/jacoco/compare/v0.8.14...v0.8.15) --- updated-dependencies: - dependency-name: org.jacoco:jacoco-maven-plugin dependency-version: 0.8.15 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ba9895367..059f2fa8a 100644 --- a/pom.xml +++ b/pom.xml @@ -182,7 +182,7 @@ org.jacoco jacoco-maven-plugin - 0.8.14 + 0.8.15 pre-unit-test From 074d5c5634689a8376b993c466ebdf1a33b59093 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:35:17 -0400 Subject: [PATCH 218/219] [chore] Release 9.10.0 (#1223) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 059f2fa8a..1323744e1 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.9.0 + 9.10.0 jar firebase-admin From 197489e1506302618ac76b074e921c4dd6174ddd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:35:48 -0400 Subject: [PATCH 219/219] chore(deps): Bump netty.version from 4.2.15.Final to 4.2.16.Final (#1225) Bumps `netty.version` from 4.2.15.Final to 4.2.16.Final. Updates `io.netty:netty-codec-http` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.15.Final...netty-4.2.16.Final) Updates `io.netty:netty-handler` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.15.Final...netty-4.2.16.Final) Updates `io.netty:netty-transport` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.15.Final...netty-4.2.16.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1323744e1..7ad9e8e78 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.15.Final + 4.2.16.Final