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 01/70] 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 02/70] 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 03/70] 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 04/70] 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 05/70] 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 06/70] 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 07/70] 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 08/70] 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 09/70] 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 10/70] 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 11/70] 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 12/70] 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 13/70] 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 14/70] 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 15/70] 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 16/70] 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 17/70] 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 18/70] 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 19/70] 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 20/70] 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 21/70] 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 29/70] 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 30/70] 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 31/70] 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 32/70] 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 33/70] [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 34/70] 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 35/70] 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 36/70] 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 37/70] 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 38/70] 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 39/70] 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 40/70] 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 41/70] 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 42/70] 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 43/70] [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 44/70] 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 45/70] 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 46/70] 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 47/70] 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 48/70] 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 49/70] 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 50/70] 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 51/70] 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 52/70] 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 53/70] 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 54/70] 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 55/70] [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 56/70] 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 From 33da83f90cb3bc6659151ee36eceb06644433159 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:34:56 -0400 Subject: [PATCH 57/70] chore: update GitHub Actions to Node 24 versions (#1232) * update GitHub Actions to Node 24 versions * Fix zizmor findings --- .github/workflows/ci.yml | 9 +++++++-- .github/workflows/nightly.yml | 14 +++++++++----- .github/workflows/release.yml | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c3d6ac73..48859acb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,9 @@ name: Continuous Integration on: pull_request +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -24,10 +27,12 @@ jobs: java-version: [8, 11, 17] steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up JDK - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'zulu' java-version: ${{ matrix.java-version }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5026f2580..29962acae 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -22,6 +22,9 @@ on: repository_dispatch: types: [firebase_nightly_build] +permissions: + contents: read + jobs: nightly: @@ -29,12 +32,13 @@ jobs: steps: - name: Checkout source for staging - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.client_payload.ref || github.ref }} + persist-credentials: false - name: Set up JDK 8 - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'zulu' java-version: 8 @@ -48,14 +52,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist path: dist - name: Send email on failure if: failure() - uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 + uses: firebase/firebase-admin-node/.github/actions/send-email@7343f166ec71c2efee5fe0554c2fb96203faf2a5 # Admin Node SDK v14.2.0 with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} @@ -70,7 +74,7 @@ jobs: - name: Send email on cancelled if: cancelled() - uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 + uses: firebase/firebase-admin-node/.github/actions/send-email@7343f166ec71c2efee5fe0554c2fb96203faf2a5 # Admin Node SDK v14.2.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 0be2d122c..4830029e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,9 @@ on: types: - firebase_build +permissions: + contents: read + jobs: stage_release: # To publish a release, merge a PR with the title prefix '[chore] Release ' to main @@ -46,10 +49,12 @@ jobs: steps: - name: Checkout source for staging - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up JDK 8 - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'zulu' java-version: 8 @@ -63,7 +68,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist path: dist @@ -86,10 +91,13 @@ jobs: steps: - name: Checkout source for publish - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Ignored since the GITHUB_TOKEN needs to be persisted for this workflow. + persist-credentials: true # zizmor: ignore[artipacked] - name: Set up JDK 8 - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'zulu' java-version: 8 From 7bea2447cc29d7f443d95ea97378b09bb8e583f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:36:16 -0400 Subject: [PATCH 58/70] chore(deps): Bump com.google.cloud:libraries-bom from 26.83.0 to 26.88.1 (#1236) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/google-cloud-java) from 26.83.0 to 26.88.1. - [Release notes](https://github.com/googleapis/google-cloud-java/releases) - [Changelog](https://github.com/googleapis/google-cloud-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-cloud-java/commits) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.88.1 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 7ad9e8e78..969987fe2 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.83.0 + 26.88.1 pom import From 5b75c5cadb96b9c99d629bb34afb6ef0bf8fb56b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 17 Sep 2026 11:21:56 -0400 Subject: [PATCH 59/70] chore(deps): Bump org.apache.httpcomponents.client5:httpclient5 (#1239) * chore(deps): Bump org.apache.httpcomponents.client5:httpclient5 Bumps [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client) from 5.3.1 to 5.6.3. - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.6.3/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.3.1...rel/v5.6.3) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: 5.6.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * test: use ServerBootstrap to construct test HttpServer * fix(transport): Enforce read timeout on HTTP/2 requests in ApacheHttp2Request * test: simplify FakeServer HttpProcessor using HttpProcessorBuilder --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- .../firebase/internal/ApacheHttp2Request.java | 11 ++- .../internal/ApacheHttp2TransportTest.java | 68 +++++++++++-------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/pom.xml b/pom.xml index 969987fe2..0e98d71fb 100644 --- a/pom.xml +++ b/pom.xml @@ -444,7 +444,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.3.1 + 5.6.3 com.nimbusds diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index ceb213c52..b06692ee8 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -49,6 +49,7 @@ final class ApacheHttp2Request extends LowLevelHttpRequest { private SimpleHttpRequest request; private final RequestConfig.Builder requestConfig; private int writeTimeout; + private int readTimeout; private ApacheHttp2AsyncEntityProducer entityProducer; private ApacheHttp2AsyncEntityConsumer entityConsumer; @@ -57,6 +58,7 @@ final class ApacheHttp2Request extends LowLevelHttpRequest { this.httpAsyncClient = httpAsyncClient; this.requestBuilder = requestBuilder; this.writeTimeout = 0; + this.readTimeout = 0; this.requestConfig = RequestConfig.custom() .setRedirectsEnabled(false); @@ -69,6 +71,7 @@ public void addHeader(String name, String value) { @Override public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { + this.readTimeout = readTimeout; requestConfig .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); @@ -125,7 +128,10 @@ public void cancelled() { // Wait for response try { - final Message response = responseFuture.get(); + final Message response = + readTimeout > 0 + ? responseFuture.get(readTimeout, TimeUnit.MILLISECONDS) + : responseFuture.get(); return new ApacheHttp2Response(response); } catch (ExecutionException e) { if (e.getCause() instanceof ConnectTimeoutException @@ -142,6 +148,9 @@ public void cancelled() { throw new IOException("Request Interrupted", e); } catch (CancellationException e) { throw new IOException("Request Cancelled", e); + } catch (TimeoutException e) { + responseFuture.cancel(true); + throw new IOException("Stream exception in request", e); } } diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java index ba604d3d1..e6d80847a 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java @@ -58,17 +58,16 @@ 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.impl.bootstrap.ServerBootstrap; 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; 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.apache.hc.core5.http.protocol.HttpProcessorBuilder; import org.junit.Assert; import org.junit.Test; @@ -423,6 +422,39 @@ public void handle( } } + @Test + public void testReadTimeout() throws Exception { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + response.setCode(HttpStatus.SC_OK); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/timeout"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpRequest request = transport.createRequestFactory() + .buildGetRequest(testUrl); + request.setReadTimeout(100); + + try { + request.execute(); + Assert.fail("Expected IOException on read timeout"); + } catch (IOException e) { + assertEquals("Stream exception in request", e.getMessage()); + } + } + } + private static class FakeServer implements AutoCloseable { private final HttpServer server; @@ -434,31 +466,11 @@ public HttpRequestHandler resolve(HttpRequest request, HttpContext context) 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 = ServerBootstrap.bootstrap() + .setListenerPort(0) + .setHttpProcessor(HttpProcessorBuilder.create().build()) + .setRequestRouter(mapper) + .create(); server.start(); } From fc35cd4420bbb0a090354ab44e6b33c12671ee67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:28:07 -0400 Subject: [PATCH 60/70] chore(deps-dev): Bump org.apache.maven.plugins:maven-surefire-plugin (#1237) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.6 to 3.6.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.6...surefire-3.6.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.6.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> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0e98d71fb..a556c9c8e 100644 --- a/pom.xml +++ b/pom.xml @@ -273,7 +273,7 @@ maven-surefire-plugin - 3.5.6 + 3.6.0 ${skipUTs} From db48c53599397561382d92fc31228d69709e803b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:32:55 -0400 Subject: [PATCH 61/70] chore(deps-dev): Bump org.apache.maven.plugins:maven-compiler-plugin (#1235) Bumps [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.15.0 to 3.16.0. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.15.0...maven-compiler-plugin-3.16.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.16.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> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a556c9c8e..d00d2020a 100644 --- a/pom.xml +++ b/pom.xml @@ -263,7 +263,7 @@ maven-compiler-plugin - 3.15.0 + 3.16.0 1.8 1.8 From 4124d0387b82d3b12964e012c832429dc201f9d3 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 24 Sep 2026 15:59:10 -0400 Subject: [PATCH 62/70] feat(fcm): Migrate topic management to FCM v1 API (#1240) * feat(messaging): topic subscription changes * refactor(messaging): reuse shared ExecutorService across topic management requests * refactor(messaging): use daemon threads and handle rejected executions * refactor(messaging): use app thread pool and sendEachAsync pattern for topic management - Delegate topic subscriptions asynchronously per token using CallableOperation.callAsync(app) in FirebaseMessaging, matching sendEachAsync pattern and respecting developer-configured ThreadManager - Remove internal ExecutorService and ThreadFactory configuration from FirebaseMessagingClientImpl - Simplify extractReason to rely on MessagingErrorHandler populating getMessagingErrorCode() without reparsing raw JSON - Map HTTP 503 to UNAVAILABLE and HTTP 504 to DEADLINE_EXCEEDED in extractReason - Normalize fcmHost during construction by stripping trailing slashes - Update unit tests in FirebaseMessagingTest and FirebaseMessagingClientImplTest * test(messaging): ensure thread-safe and order-independent topic batch assertions --- .../firebase/messaging/FirebaseMessaging.java | 189 ++++++++++++- .../messaging/FirebaseMessagingClient.java | 19 ++ .../FirebaseMessagingClientImpl.java | 76 +++++- .../messaging/TopicManagementResponse.java | 7 +- .../FirebaseMessagingClientImplTest.java | 104 ++++++++ .../messaging/FirebaseMessagingTest.java | 249 +++++++++++++++--- 6 files changed, 592 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 0e9831588..b38e8d827 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -537,7 +537,11 @@ FirebaseMessagingClient getMessagingClient() { */ public TopicManagementResponse subscribeToTopic(@NonNull List registrationTokens, @NonNull String topic) throws FirebaseMessagingException { - return subscribeOp(registrationTokens, topic).call(); + try { + return subscribeToTopicAsync(registrationTokens, topic).get(); + } catch (InterruptedException | ExecutionException e) { + throw new FirebaseMessagingException(ErrorCode.CANCELLED, SERVICE_ID); + } } /** @@ -550,10 +554,33 @@ public TopicManagementResponse subscribeToTopic(@NonNull List registrati */ public ApiFuture subscribeToTopicAsync( @NonNull List registrationTokens, @NonNull String topic) { - return subscribeOp(registrationTokens, topic).callAsync(app); + return manageTopicAsync(registrationTokens, topic, true); + } + + /** + * Subscribes a list of registration tokens to a topic using the legacy Instance ID API. + * + * @deprecated Use {@link #subscribeToTopic(List, String)} instead. + */ + @Deprecated + public TopicManagementResponse subscribeToTopicLegacy(@NonNull List registrationTokens, + @NonNull String topic) throws FirebaseMessagingException { + return subscribeLegacyOp(registrationTokens, topic).call(); + } + + /** + * Similar to {@link #subscribeToTopicLegacy(List, String)} but performs the operation + * asynchronously. + * + * @deprecated Use {@link #subscribeToTopicAsync(List, String)} instead. + */ + @Deprecated + public ApiFuture subscribeToTopicLegacyAsync( + @NonNull List registrationTokens, @NonNull String topic) { + return subscribeLegacyOp(registrationTokens, topic).callAsync(app); } - private CallableOperation subscribeOp( + private CallableOperation subscribeLegacyOp( final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); @@ -576,7 +603,11 @@ protected TopicManagementResponse execute() throws FirebaseMessagingException { */ public TopicManagementResponse unsubscribeFromTopic(@NonNull List registrationTokens, @NonNull String topic) throws FirebaseMessagingException { - return unsubscribeOp(registrationTokens, topic).call(); + try { + return unsubscribeFromTopicAsync(registrationTokens, topic).get(); + } catch (InterruptedException | ExecutionException e) { + throw new FirebaseMessagingException(ErrorCode.CANCELLED, SERVICE_ID); + } } /** @@ -590,11 +621,155 @@ public TopicManagementResponse unsubscribeFromTopic(@NonNull List regist */ public ApiFuture unsubscribeFromTopicAsync( @NonNull List registrationTokens, @NonNull String topic) { - return unsubscribeOp(registrationTokens, topic).callAsync(app); + return manageTopicAsync(registrationTokens, topic, false); } - private CallableOperation unsubscribeOp( - final List registrationTokens, final String topic) { + private ApiFuture manageTopicAsync( + final List registrationTokens, final String topic, final boolean isSubscribe) { + checkRegistrationTokens(registrationTokens); + checkTopic(topic); + final String cleanTopic = topic.startsWith("/topics/") + ? topic.substring("/topics/".length()) : topic; + final List immutableTokens = ImmutableList.copyOf(registrationTokens); + + List> futures = new ArrayList<>(immutableTokens.size()); + for (int i = 0; i < immutableTokens.size(); i++) { + futures.add( + manageTopicSingleOp(immutableTokens.get(i), cleanTopic, isSubscribe, i) + .callAsync(app)); + } + + ApiFuture> resultsFuture = ApiFutures.allAsList(futures); + return ApiFutures.transform( + resultsFuture, + (results) -> { + int successCount = 0; + List errors = new ArrayList<>(); + for (TopicResult result : results) { + if (result.isSuccess()) { + successCount++; + } else { + errors.add(new TopicManagementResponse.Error( + result.getIndex(), result.getReason())); + } + } + return new TopicManagementResponse(successCount, errors); + }, + MoreExecutors.directExecutor()); + } + + private CallableOperation manageTopicSingleOp( + final String token, final String topic, final boolean isSubscribe, final int index) { + final FirebaseMessagingClient messagingClient = getMessagingClient(); + return new CallableOperation() { + @Override + protected TopicResult execute() { + try { + if (isSubscribe) { + messagingClient.subscribeToTopic(topic, token); + } else { + messagingClient.unsubscribeFromTopic(topic, token); + } + return TopicResult.success(index); + } catch (FirebaseMessagingException e) { + return TopicResult.error(index, extractReason(e)); + } catch (Exception e) { + return TopicResult.error(index, "UNKNOWN_ERROR"); + } + } + }; + } + + @VisibleForTesting + static String extractReason(FirebaseMessagingException e) { + if (e.getMessagingErrorCode() != null) { + return e.getMessagingErrorCode().name(); + } + if (e.getErrorCode() != null && e.getErrorCode() != ErrorCode.UNKNOWN) { + return e.getErrorCode().name(); + } + if (e.getHttpResponse() != null) { + switch (e.getHttpResponse().getStatusCode()) { + case 400: + return "INVALID_ARGUMENT"; + case 401: + case 403: + return "PERMISSION_DENIED"; + case 404: + return "NOT_FOUND"; + case 429: + return "RESOURCE_EXHAUSTED"; + case 500: + return "INTERNAL"; + case 503: + return "UNAVAILABLE"; + case 504: + return "DEADLINE_EXCEEDED"; + default: + return "UNKNOWN_ERROR"; + } + } + return "UNKNOWN_ERROR"; + } + + private static class TopicResult { + private final int index; + private final boolean success; + private final String reason; + + private TopicResult(int index, boolean success, String reason) { + this.index = index; + this.success = success; + this.reason = reason; + } + + static TopicResult success(int index) { + return new TopicResult(index, true, null); + } + + static TopicResult error(int index, String reason) { + return new TopicResult(index, false, reason); + } + + int getIndex() { + return index; + } + + boolean isSuccess() { + return success; + } + + String getReason() { + return reason; + } + } + + /** + * Unsubscribes a list of registration tokens from a topic using the legacy Instance ID API. + * + * @deprecated Use {@link #unsubscribeFromTopic(List, String)} instead. + */ + @Deprecated + public TopicManagementResponse unsubscribeFromTopicLegacy( + @NonNull List registrationTokens, + @NonNull String topic) throws FirebaseMessagingException { + return unsubscribeLegacyOp(registrationTokens, topic).call(); + } + + /** + * Similar to {@link #unsubscribeFromTopicLegacy(List, String)} but performs the operation + * asynchronously. + * + * @deprecated Use {@link #unsubscribeFromTopicAsync(List, String)} instead. + */ + @Deprecated + public ApiFuture unsubscribeFromTopicLegacyAsync( + @NonNull List registrationTokens, @NonNull String topic) { + return unsubscribeLegacyOp(registrationTokens, topic).callAsync(app); + } + + private CallableOperation + unsubscribeLegacyOp(final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); final InstanceIdClient instanceIdClient = getInstanceIdClient(); diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java index da049565d..a4e436a61 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java @@ -29,4 +29,23 @@ interface FirebaseMessagingClient { */ BatchResponse sendAll(List messages, boolean dryRun) throws FirebaseMessagingException; + /** + * Subscribes a registration token to a topic via the FCM v1 API. + * + * @param topic Name of the topic. + * @param registrationToken A registration token. + * @throws FirebaseMessagingException If an error occurs. + */ + void subscribeToTopic( + String topic, String registrationToken) throws FirebaseMessagingException; + + /** + * Unsubscribes a registration token from a topic via the FCM v1 API. + * + * @param topic Name of the topic. + * @param registrationToken A registration token. + * @throws FirebaseMessagingException If an error occurs. + */ + void unsubscribeFromTopic( + String topic, String registrationToken) throws FirebaseMessagingException; } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index 6049b4f4d..9b38ac769 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -52,6 +52,8 @@ import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; import com.google.firebase.messaging.internal.MessagingServiceResponse; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -60,13 +62,16 @@ */ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { - private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + private static final String DEFAULT_FCM_HOST = "https://fcm.googleapis.com"; + private static final String FCM_URL = "%s/v1/projects/%s/messages:send"; private static final Map COMMON_HEADERS = ImmutableMap.of( "X-GOOG-API-FORMAT-VERSION", "2", "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion()); + private final String projectId; + private final String fcmHost; private final String fcmSendUrl; private final HttpRequestFactory requestFactory; private final HttpRequestFactory childRequestFactory; @@ -78,7 +83,13 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { private FirebaseMessagingClientImpl(Builder builder) { checkArgument(!Strings.isNullOrEmpty(builder.projectId)); - this.fcmSendUrl = String.format(FCM_URL, builder.projectId); + this.projectId = builder.projectId; + String host = Strings.isNullOrEmpty(builder.fcmHost) ? DEFAULT_FCM_HOST : builder.fcmHost; + while (host.endsWith("/")) { + host = host.substring(0, host.length() - 1); + } + this.fcmHost = host; + this.fcmSendUrl = String.format(FCM_URL, this.fcmHost, builder.projectId); this.requestFactory = checkNotNull(builder.requestFactory); this.childRequestFactory = checkNotNull(builder.childRequestFactory); this.jsonFactory = checkNotNull(builder.jsonFactory); @@ -182,6 +193,61 @@ public void initialize(HttpRequest request) throws IOException { }; } + @Override + public void subscribeToTopic( + String topic, String registrationToken) throws FirebaseMessagingException { + sendSingleTopicRequest(registrationToken, topic, true); + } + + @Override + public void unsubscribeFromTopic( + String topic, String registrationToken) throws FirebaseMessagingException { + sendSingleTopicRequest(registrationToken, topic, false); + } + + private void sendSingleTopicRequest( + String token, String topic, boolean isSubscribe) throws FirebaseMessagingException { + try { + String topicName = topic.startsWith("/topics/") + ? topic.substring("/topics/".length()) : topic; + String encodedToken = URLEncoder.encode(token, StandardCharsets.UTF_8.name()); + String encodedTopic = URLEncoder.encode(topicName, StandardCharsets.UTF_8.name()); + HttpRequestInfo requestInfo; + if (isSubscribe) { + String url = String.format( + "%s/v1/projects/%s/registrations/%s/topicSubscriptions?topic_name=%s", + fcmHost, projectId, encodedToken, encodedTopic); + requestInfo = HttpRequestInfo.buildJsonPostRequest(url, ImmutableMap.of()) + .addAllHeaders(COMMON_HEADERS); + } else { + String url = String.format( + "%s/v1/projects/%s/registrations/%s/topicSubscriptions/%s?allow_missing=true", + fcmHost, projectId, encodedToken, encodedTopic); + requestInfo = HttpRequestInfo.buildDeleteRequest(url) + .addAllHeaders(COMMON_HEADERS); + } + + httpClient.send(requestInfo); + } catch (FirebaseMessagingException e) { + if (isSubscribe && isAlreadyExists(e)) { + return; + } + throw e; + } catch (IOException e) { + throw errorHandler.handleIOException(e); + } + } + + private boolean isAlreadyExists(FirebaseMessagingException e) { + if (e.getHttpResponse() != null && e.getHttpResponse().getStatusCode() == 409) { + return true; + } + if (e.getErrorCode() == ErrorCode.ALREADY_EXISTS || e.getErrorCode() == ErrorCode.CONFLICT) { + return true; + } + return false; + } + static FirebaseMessagingClientImpl fromApp(FirebaseApp app) { String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), @@ -203,6 +269,7 @@ static Builder builder() { static final class Builder { private String projectId; + private String fcmHost = DEFAULT_FCM_HOST; private HttpRequestFactory requestFactory; private HttpRequestFactory childRequestFactory; private JsonFactory jsonFactory; @@ -215,6 +282,11 @@ Builder setProjectId(String projectId) { return this; } + Builder setFcmHost(String fcmHost) { + this.fcmHost = fcmHost; + return this; + } + Builder setRequestFactory(HttpRequestFactory requestFactory) { this.requestFactory = requestFactory; return this; diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index f02590f74..28664efce 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -61,6 +61,11 @@ public class TopicManagementResponse { this.errors = errors.build(); } + TopicManagementResponse(int successCount, List errors) { + this.successCount = successCount; + this.errors = ImmutableList.copyOf(errors); + } + /** * Gets the number of registration tokens that were successfully subscribed or unsubscribed. * @@ -97,7 +102,7 @@ public static class Error { private final int index; private final String reason; - private Error(int index, String reason) { + Error(int index, String reason) { this.index = index; if (reason == null || reason.trim().isEmpty()) { this.reason = UNKNOWN_ERROR; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 03bfc4327..8052beb36 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -651,4 +651,108 @@ private static Map> buildTestMessages() { return builder.build(); } + + @Test + public void testSubscribeToTopic() throws Exception { + response.setContent("{}"); + client.subscribeToTopic("test-topic", "id1"); + + HttpRequest request = interceptor.getLastRequest(); + assertEquals("POST", request.getRequestMethod()); + assertEquals( + "https://fcm.googleapis.com/v1/projects/test-project/registrations/id1" + + "/topicSubscriptions?topic_name=test-topic", + request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + } + + @Test + public void testSubscribeToTopic409() throws Exception { + response.setStatusCode(409).setContent("{\"error\": {\"status\": \"ALREADY_EXISTS\"}}"); + client.subscribeToTopic("test-topic", "id1"); + } + + @Test + public void testUnsubscribeFromTopic() throws Exception { + response.setContent("{}"); + client.unsubscribeFromTopic("test-topic", "id1"); + + HttpRequest request = interceptor.getLastRequest(); + assertEquals("DELETE", request.getRequestMethod()); + assertEquals( + "https://fcm.googleapis.com/v1/projects/test-project/registrations/id1" + + "/topicSubscriptions/test-topic?allow_missing=true", + request.getUrl().toString()); + } + + @Test + public void testUnsubscribeFromTopic404() { + response.setStatusCode(404).setContent("{\"error\": {\"status\": \"NOT_FOUND\"}}"); + try { + client.unsubscribeFromTopic("test-topic", "id1"); + fail("No error thrown"); + } catch (FirebaseMessagingException e) { + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + } + } + + @Test + public void testTopicManagementFcmErrorDetails() { + response.setStatusCode(404).setContent("{\n" + + " \"error\": {\n" + + " \"status\": \"NOT_FOUND\",\n" + + " \"details\": [\n" + + " {\n" + + " \"@type\": \"type.googleapis.com/google.firebase.fcm.v1.FcmError\",\n" + + " \"errorCode\": \"UNREGISTERED\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"); + try { + client.subscribeToTopic("test-topic", "id1"); + fail("No error thrown"); + } catch (FirebaseMessagingException e) { + assertEquals(MessagingErrorCode.UNREGISTERED, e.getMessagingErrorCode()); + } + } + + @Test + public void testTopicManagement500Error() { + response.setStatusCode(500).setContent("{}"); + try { + client.subscribeToTopic("test-topic", "id1"); + fail("No error thrown"); + } catch (FirebaseMessagingException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + } + } + + @Test + public void testFcmHostTrailingSlash() throws Exception { + TestResponseInterceptor testInterceptor = new TestResponseInterceptor(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("{}")) + .build(); + FirebaseMessagingClientImpl clientWithSlash = FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setFcmHost("https://custom.fcm.host///") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) + .setResponseInterceptor(testInterceptor) + .build(); + + clientWithSlash.subscribeToTopic("test-topic", "id1"); + HttpRequest request = testInterceptor.getLastRequest(); + assertEquals( + "https://custom.fcm.host/v1/projects/test-project/registrations/id1" + + "/topicSubscriptions?topic_name=test-topic", + request.getUrl().toString()); + assertEquals( + "https://custom.fcm.host/v1/projects/test-project/messages:send", + clientWithSlash.getFcmSendUrl()); + } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 42a499b75..02e05fdd6 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -26,23 +26,28 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.json.GenericJson; 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.common.collect.ImmutableSet; 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.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.Nullable; - +import java.util.ArrayList; 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; @@ -547,9 +552,9 @@ public void testSendEachForMulticastAsyncFailure() throws Exception { @Test public void testInvalidSubscribe() throws FirebaseMessagingException { - MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromResponse(null); FirebaseMessaging messaging = getMessagingForTopicManagement( - Suppliers.ofInstance(client)); + Suppliers.ofInstance(client)); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { @@ -565,55 +570,93 @@ public void testInvalidSubscribe() throws FirebaseMessagingException { @Test public void testSubscribeToTopic() throws FirebaseMessagingException { - MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + MockFirebaseMessagingClient client = + MockFirebaseMessagingClient.fromResponse(TOPIC_MGT_RESPONSE); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); TopicManagementResponse got = messaging.subscribeToTopic( ImmutableList.of("id1", "id2"), "test-topic"); - assertSame(TOPIC_MGT_RESPONSE, got); + assertEquals(2, got.getSuccessCount()); + assertEquals(0, got.getFailureCount()); + assertTrue(got.getErrors().isEmpty()); + assertEquals("test-topic", client.lastTopic); + assertEquals(ImmutableSet.of("id1", "id2"), ImmutableSet.copyOf(client.lastBatch)); } @Test - public void testSubscribeToTopicFailure() { - MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + public void testSubscribeToTopicFailure() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - try { - messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } catch (FirebaseMessagingException e) { - assertSame(TEST_EXCEPTION, e); - } + TopicManagementResponse got = messaging.subscribeToTopic( + ImmutableList.of("id1", "id2"), "test-topic"); + + assertEquals(0, got.getSuccessCount()); + assertEquals(2, got.getFailureCount()); + assertEquals(2, got.getErrors().size()); + assertEquals(0, got.getErrors().get(0).getIndex()); + assertEquals(1, got.getErrors().get(1).getIndex()); } @Test public void testSubscribeToTopicAsync() throws Exception { - MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + MockFirebaseMessagingClient client = + MockFirebaseMessagingClient.fromResponse(TOPIC_MGT_RESPONSE); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); TopicManagementResponse got = messaging.subscribeToTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - assertSame(TOPIC_MGT_RESPONSE, got); + assertEquals(2, got.getSuccessCount()); + assertEquals(0, got.getFailureCount()); + assertTrue(got.getErrors().isEmpty()); + assertEquals("test-topic", client.lastTopic); + assertEquals(ImmutableSet.of("id1", "id2"), ImmutableSet.copyOf(client.lastBatch)); } @Test - public void testSubscribeToTopicAsyncFailure() throws InterruptedException { - MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + public void testSubscribeToTopicAsyncFailure() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - } catch (ExecutionException e) { - assertSame(TEST_EXCEPTION, e.getCause()); - } + TopicManagementResponse got = messaging.subscribeToTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertEquals(0, got.getSuccessCount()); + assertEquals(2, got.getFailureCount()); + assertEquals(2, got.getErrors().size()); + } + + @Test + public void testSubscribeToTopicLegacy() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = + getMessagingForLegacyTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.subscribeToTopicLegacy( + ImmutableList.of("id1", "id2"), "test-topic"); + + assertSame(TOPIC_MGT_RESPONSE, got); + } + + @Test + public void testSubscribeToTopicLegacyAsync() throws Exception { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = + getMessagingForLegacyTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.subscribeToTopicLegacyAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertSame(TOPIC_MGT_RESPONSE, got); } @Test public void testInvalidUnsubscribe() throws FirebaseMessagingException { - MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromResponse(null); FirebaseMessaging messaging = getMessagingForTopicManagement( - Suppliers.ofInstance(client)); + Suppliers.ofInstance(client)); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { @@ -629,48 +672,121 @@ public void testInvalidUnsubscribe() throws FirebaseMessagingException { @Test public void testUnsubscribeFromTopic() throws FirebaseMessagingException { - MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + MockFirebaseMessagingClient client = + MockFirebaseMessagingClient.fromResponse(TOPIC_MGT_RESPONSE); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); TopicManagementResponse got = messaging.unsubscribeFromTopic( ImmutableList.of("id1", "id2"), "test-topic"); - assertSame(TOPIC_MGT_RESPONSE, got); + assertEquals(2, got.getSuccessCount()); + assertEquals(0, got.getFailureCount()); + assertTrue(got.getErrors().isEmpty()); + assertEquals("test-topic", client.lastTopic); + assertEquals(ImmutableSet.of("id1", "id2"), ImmutableSet.copyOf(client.lastBatch)); } @Test - public void testUnsubscribeFromTopicFailure() { - MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + public void testUnsubscribeFromTopicFailure() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - try { - messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } catch (FirebaseMessagingException e) { - assertSame(TEST_EXCEPTION, e); - } + TopicManagementResponse got = messaging.unsubscribeFromTopic( + ImmutableList.of("id1", "id2"), "test-topic"); + + assertEquals(0, got.getSuccessCount()); + assertEquals(2, got.getFailureCount()); + assertEquals(2, got.getErrors().size()); + assertEquals(0, got.getErrors().get(0).getIndex()); + assertEquals(1, got.getErrors().get(1).getIndex()); } @Test public void testUnsubscribeFromTopicAsync() throws Exception { - MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + MockFirebaseMessagingClient client = + MockFirebaseMessagingClient.fromResponse(TOPIC_MGT_RESPONSE); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); TopicManagementResponse got = messaging.unsubscribeFromTopicAsync( ImmutableList.of("id1", "id2"), "test-topic").get(); - assertSame(TOPIC_MGT_RESPONSE, got); + assertEquals(2, got.getSuccessCount()); + assertEquals(0, got.getFailureCount()); + assertTrue(got.getErrors().isEmpty()); + assertEquals("test-topic", client.lastTopic); + assertEquals(ImmutableSet.of("id1", "id2"), ImmutableSet.copyOf(client.lastBatch)); } @Test - public void testUnsubscribeFromTopicAsyncFailure() throws InterruptedException { - MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + public void testUnsubscribeFromTopicAsyncFailure() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - } catch (ExecutionException e) { - assertSame(TEST_EXCEPTION, e.getCause()); - } + TopicManagementResponse got = messaging.unsubscribeFromTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertEquals(0, got.getSuccessCount()); + assertEquals(2, got.getFailureCount()); + assertEquals(2, got.getErrors().size()); + } + + @Test + public void testExtractReason() { + FirebaseMessagingException messagingError = + FirebaseMessagingException.withMessagingErrorCode( + new FirebaseException(ErrorCode.INVALID_ARGUMENT, "bad arg", null), + MessagingErrorCode.UNREGISTERED); + assertEquals("UNREGISTERED", FirebaseMessaging.extractReason(messagingError)); + + FirebaseMessagingException platformError = + new FirebaseMessagingException(ErrorCode.PERMISSION_DENIED, "permission denied"); + assertEquals("PERMISSION_DENIED", FirebaseMessaging.extractReason(platformError)); + + IncomingHttpResponse resp503 = new IncomingHttpResponse( + new HttpResponseException.Builder(503, "Unavailable", new HttpHeaders()).build(), + new OutgoingHttpRequest("GET", "https://example.com")); + FirebaseMessagingException unavailableError = + FirebaseMessagingException.withMessagingErrorCode( + new FirebaseException(ErrorCode.UNKNOWN, "unavailable", null, resp503), + null); + assertEquals("UNAVAILABLE", FirebaseMessaging.extractReason(unavailableError)); + + IncomingHttpResponse resp504 = new IncomingHttpResponse( + new HttpResponseException.Builder(504, "Gateway Timeout", new HttpHeaders()).build(), + new OutgoingHttpRequest("GET", "https://example.com")); + FirebaseMessagingException timeoutError = + FirebaseMessagingException.withMessagingErrorCode( + new FirebaseException(ErrorCode.UNKNOWN, "timeout", null, resp504), + null); + assertEquals("DEADLINE_EXCEEDED", FirebaseMessaging.extractReason(timeoutError)); + + FirebaseMessagingException unknownError = + new FirebaseMessagingException(ErrorCode.UNKNOWN, "something unknown"); + assertEquals("UNKNOWN_ERROR", FirebaseMessaging.extractReason(unknownError)); + } + + @Test + public void testUnsubscribeFromTopicLegacy() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = + getMessagingForLegacyTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.unsubscribeFromTopicLegacy( + ImmutableList.of("id1", "id2"), "test-topic"); + + assertSame(TOPIC_MGT_RESPONSE, got); + } + + @Test + public void testUnsubscribeFromTopicLegacyAsync() throws Exception { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = + getMessagingForLegacyTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.unsubscribeFromTopicLegacyAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertSame(TOPIC_MGT_RESPONSE, got); } private FirebaseMessaging getMessagingForSend( @@ -684,6 +800,16 @@ private FirebaseMessaging getMessagingForSend( } private FirebaseMessaging getMessagingForTopicManagement( + Supplier supplier) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(supplier) + .setInstanceIdClient(Suppliers.ofInstance(null)) + .build(); + } + + private FirebaseMessaging getMessagingForLegacyTopicManagement( Supplier supplier) { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); return FirebaseMessaging.builder() @@ -697,11 +823,14 @@ private static class MockFirebaseMessagingClient implements FirebaseMessagingCli private String messageId; private BatchResponse batchResponse; + private TopicManagementResponse topicManagementResponse; private FirebaseMessagingException exception; private Message lastMessage; private boolean isLastDryRun; private ImmutableMap messageMap; + private String lastTopic; + private List lastBatch; private MockFirebaseMessagingClient( String messageId, BatchResponse batchResponse, FirebaseMessagingException exception) { @@ -710,6 +839,12 @@ private MockFirebaseMessagingClient( this.exception = exception; } + private MockFirebaseMessagingClient( + TopicManagementResponse topicManagementResponse, FirebaseMessagingException exception) { + this.topicManagementResponse = topicManagementResponse; + this.exception = exception; + } + private MockFirebaseMessagingClient( Map messageMap, FirebaseMessagingException exception) { this.messageMap = ImmutableMap.copyOf(messageMap); @@ -720,6 +855,10 @@ static MockFirebaseMessagingClient fromMessageId(String messageId) { return new MockFirebaseMessagingClient(messageId, null, null); } + static MockFirebaseMessagingClient fromResponse(TopicManagementResponse response) { + return new MockFirebaseMessagingClient(response, null); + } + static MockFirebaseMessagingClient fromMessageMap(Map messageMap) { return new MockFirebaseMessagingClient(messageMap, null); } @@ -753,6 +892,32 @@ public BatchResponse sendAll( List messages, boolean dryRun) throws FirebaseMessagingException { return batchResponse; } + + @Override + public synchronized void subscribeToTopic( + String topic, String registrationToken) throws FirebaseMessagingException { + this.lastTopic = topic; + if (this.lastBatch == null) { + this.lastBatch = new ArrayList<>(); + } + this.lastBatch.add(registrationToken); + if (exception != null) { + throw exception; + } + } + + @Override + public synchronized void unsubscribeFromTopic( + String topic, String registrationToken) throws FirebaseMessagingException { + this.lastTopic = topic; + if (this.lastBatch == null) { + this.lastBatch = new ArrayList<>(); + } + this.lastBatch.add(registrationToken); + if (exception != null) { + throw exception; + } + } } private static class MockInstanceIdClient implements InstanceIdClient { From e8a7ebd860fcda0833de90bc129713bd4dc84b6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:11:34 -0400 Subject: [PATCH 63/70] chore(deps-dev): Bump org.apache.maven.plugins:maven-failsafe-plugin (#1244) Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.6 to 3.6.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.6...surefire-3.6.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.6.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: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d00d2020a..cb8e055f0 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,7 @@ maven-failsafe-plugin - 3.5.6 + 3.6.0 From 3b6dc6588742036edd3d5dfd7b03301c81c5f24f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:17:27 -0400 Subject: [PATCH 64/70] chore(deps): Bump netty.version from 4.2.16.Final to 4.2.18.Final (#1243) Bumps `netty.version` from 4.2.16.Final to 4.2.18.Final. Updates `io.netty:netty-codec-http` from 4.2.16.Final to 4.2.18.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.18.Final) Updates `io.netty:netty-handler` from 4.2.16.Final to 4.2.18.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.18.Final) Updates `io.netty:netty-transport` from 4.2.16.Final to 4.2.18.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.18.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.18.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.18.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.18.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 cb8e055f0..b6a0a014c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.16.Final + 4.2.18.Final From b65a397bdd6215ec37d4ba3c6ae31fe4e03f4c09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:21:50 -0400 Subject: [PATCH 65/70] chore(deps): Bump org.slf4j:slf4j-api from 2.0.18 to 2.0.19 (#1242) Bumps org.slf4j:slf4j-api from 2.0.18 to 2.0.19. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-version: 2.0.19 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 b6a0a014c..317a20d83 100644 --- a/pom.xml +++ b/pom.xml @@ -424,7 +424,7 @@ org.slf4j slf4j-api - 2.0.18 + 2.0.19 io.netty From d0ebabd6601d372bfa43e07a3e4861a896824022 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:27:24 -0400 Subject: [PATCH 66/70] chore(deps): Bump org.apache.httpcomponents.client5:httpclient5 (#1241) Bumps [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client) from 5.6.3 to 5.6.4. - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.6.4/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.6.3...rel/v5.6.4) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: 5.6.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> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 317a20d83..675dd6945 100644 --- a/pom.xml +++ b/pom.xml @@ -444,7 +444,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.6.3 + 5.6.4 com.nimbusds From d053d2544cdcd3281a9c66631af4fd5afb7e9c7e Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 24 Sep 2026 17:38:33 -0400 Subject: [PATCH 67/70] [chore] Release 9.11.0 (#1245) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 675dd6945..508605d51 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.10.0 + 9.11.0 jar firebase-admin From e79eafb8ff0ad704bf4bc4d37b1e98788f702537 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 24 Sep 2026 18:37:18 -0400 Subject: [PATCH 68/70] [chore] Release 9.11.0 Take 2 (#1246) * fix: skip integration tests during release publish * chore: publish artifacts to Maven Central before creating release tag * chore: address review feedback on release test skipping --- .github/workflows/release.yml | 18 +++++++++--------- pom.xml | 5 +++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4830029e8..7a64bd46a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,6 +106,15 @@ jobs: id: preflight run: ./.github/scripts/publish_preflight_check.sh + - name: Publish to Maven Central + run: ./.github/scripts/publish_artifacts.sh + 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 }} + # See: https://cli.github.com/manual/gh_release_create - name: Create release tag env: @@ -116,12 +125,3 @@ jobs: gh release create "$RELEASE_VER" \ --title "Firebase Admin Java SDK $RELEASE_VER" \ --notes "$RELEASE_BODY" - - - name: Publish to Maven Central - run: ./.github/scripts/publish_artifacts.sh - 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 }} diff --git a/pom.xml b/pom.xml index 508605d51..66da562a1 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,9 @@ UTF-8 UTF-8 + false ${skipTests} + ${skipTests} 4.2.18.Final @@ -146,6 +148,9 @@ release + + true + From 02c48aaba0e657ac0f3a1d49546b614791da7fcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:33:53 -0400 Subject: [PATCH 69/70] chore(deps): Bump org.slf4j:slf4j-api from 2.0.19 to 2.0.20 (#1249) Bumps org.slf4j:slf4j-api from 2.0.19 to 2.0.20. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-version: 2.0.20 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 66da562a1..ca1a634ff 100644 --- a/pom.xml +++ b/pom.xml @@ -429,7 +429,7 @@ org.slf4j slf4j-api - 2.0.19 + 2.0.20 io.netty From 2225373d97869e72b164a4081968027c130c782b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:59:17 -0400 Subject: [PATCH 70/70] chore(deps): Bump com.nimbusds:nimbus-jose-jwt from 10.9.1 to 10.10 (#1248) Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 10.9.1 to 10.10. - [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt) - [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/10.10..10.9.1) --- updated-dependencies: - dependency-name: com.nimbusds:nimbus-jose-jwt dependency-version: '10.10' 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 ca1a634ff..f84daffa4 100644 --- a/pom.xml +++ b/pom.xml @@ -454,7 +454,7 @@ com.nimbusds nimbus-jose-jwt - 10.9.1 + 10.10