From 267d92f9c90530c096c0557211460d6b162417a4 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 29 May 2018 15:36:00 -0700 Subject: [PATCH 001/441] Removed deprecated class; Updated broken links (#180) * Removed deprecated class; Updated broken links * Snippets for user import functionality (#181) * Adding snippets for the importUsers() API * Updated snippet comment * Updated snippets for clarity --- .../firebase/cloud/FirestoreClient.java | 6 +- .../google/firebase/cloud/StorageClient.java | 4 +- .../com/google/firebase/database/Logger.java | 38 ---- .../snippets/FirebaseAuthSnippets.java | 200 ++++++++++++++++++ 4 files changed, 205 insertions(+), 43 deletions(-) delete mode 100644 src/main/java/com/google/firebase/database/Logger.java diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index ace91b40c..422b6d7de 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -13,7 +13,7 @@ /** * {@code FirestoreClient} provides access to Google Cloud Firestore. Use this API to obtain a - * {@code Firestore} + * {@code Firestore} * instance, which provides methods for updating and querying data in Firestore. * *

A Google Cloud project ID is required to access Firestore. FirestoreClient determines the @@ -44,7 +44,7 @@ private FirestoreClient(FirebaseApp app) { /** * Returns the Firestore instance associated with the default Firebase app. * - * @return A non-null {@code Firestore} + * @return A non-null {@code Firestore} * instance. */ @NonNull @@ -56,7 +56,7 @@ public static Firestore getFirestore() { * Returns the Firestore instance associated with the specified Firebase app. * * @param app A non-null {@link FirebaseApp}. - * @return A non-null {@code Firestore} + * @return A non-null {@code Firestore} * instance. */ @NonNull diff --git a/src/main/java/com/google/firebase/cloud/StorageClient.java b/src/main/java/com/google/firebase/cloud/StorageClient.java index 96b53c8da..d6a1567f4 100644 --- a/src/main/java/com/google/firebase/cloud/StorageClient.java +++ b/src/main/java/com/google/firebase/cloud/StorageClient.java @@ -71,7 +71,7 @@ public static synchronized StorageClient getInstance(FirebaseApp app) { * configured via {@link com.google.firebase.FirebaseOptions} when initializing the app. If * no bucket was configured via options, this method throws an exception. * - * @return a cloud storage {@code Bucket} + * @return a cloud storage {@code Bucket} * instance. * @throws IllegalArgumentException If no bucket is configured via FirebaseOptions, * or if the bucket does not exist. @@ -84,7 +84,7 @@ public Bucket bucket() { * Returns a cloud storage Bucket instance for the specified bucket name. * * @param name a non-null, non-empty bucket name. - * @return a cloud storage {@code Bucket} + * @return a cloud storage {@code Bucket} * instance. * @throws IllegalArgumentException If the bucket name is null, empty, or if the specified * bucket does not exist. diff --git a/src/main/java/com/google/firebase/database/Logger.java b/src/main/java/com/google/firebase/database/Logger.java deleted file mode 100644 index aa56ed8dd..000000000 --- a/src/main/java/com/google/firebase/database/Logger.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2017 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.database; - -/** - * This interface is used to setup logging for Firebase Database. - * - * @deprecated Use SLF4J-based logging - */ -public interface Logger { - - /** - * The log levels used by the Firebase Database library - * - * @deprecated Use SLF4J-based logging - */ - enum Level { - DEBUG, - INFO, - WARN, - ERROR, - NONE - } -} diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index 52c78de8c..153507dab 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -16,19 +16,33 @@ package com.google.firebase.snippets; +import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.ErrorInfo; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.ImportUserRecord; import com.google.firebase.auth.ListUsersPage; import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.auth.UserImportOptions; +import com.google.firebase.auth.UserImportResult; +import com.google.firebase.auth.UserProvider; import com.google.firebase.auth.UserRecord; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.hash.Bcrypt; +import com.google.firebase.auth.hash.HmacSha256; +import com.google.firebase.auth.hash.Pbkdf2Sha256; +import com.google.firebase.auth.hash.Scrypt; +import com.google.firebase.auth.hash.StandardScrypt; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.ws.rs.Consumes; @@ -396,4 +410,190 @@ public Response clearSessionCookieAndRevoke(@CookieParam("session") Cookie cooki } } // [END session_clear_and_revoke] + + public void importUsers() { + // [START build_user_list] + // Up to 1000 users can be imported at once. + List users = new ArrayList<>(); + users.add(ImportUserRecord.builder() + .setUid("uid1") + .setEmail("user1@example.com") + .setPasswordHash("passwordHash1".getBytes()) + .setPasswordSalt("salt1".getBytes()) + .build()); + users.add(ImportUserRecord.builder() + .setUid("uid2") + .setEmail("user2@example.com") + .setPasswordHash("passwordHash2".getBytes()) + .setPasswordSalt("salt2".getBytes()) + .build()); + // [END build_user_list] + + // [START import_users] + UserImportOptions options = UserImportOptions.withHash( + HmacSha256.builder() + .setKey("secretKey".getBytes()) + .build()); + try { + UserImportResult result = FirebaseAuth.getInstance().importUsers(users, options); + System.out.println("Successfully imported " + result.getSuccessCount() + " users"); + System.out.println("Failed to import " + result.getFailureCount() + " users"); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user at index: " + indexedError.getIndex() + + " due to error: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + // Some unrecoverable error occurred that prevented the operation from running. + } + // [END import_users] + } + + public void importWithHmac() { + // [START import_with_hmac] + try { + List users = Collections.singletonList(ImportUserRecord.builder() + .setUid("some-uid") + .setEmail("user@example.com") + .setPasswordHash("password-hash".getBytes()) + .setPasswordSalt("salt".getBytes()) + .build()); + UserImportOptions options = UserImportOptions.withHash( + HmacSha256.builder() + .setKey("secret".getBytes()) + .build()); + UserImportResult result = FirebaseAuth.getInstance().importUsers(users, options); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + System.out.println("Error importing users: " + e.getMessage()); + } + // [END import_with_hmac] + } + + public void importWithPbkdf() { + // [START import_with_pbkdf] + try { + List users = Collections.singletonList(ImportUserRecord.builder() + .setUid("some-uid") + .setEmail("user@example.com") + .setPasswordHash("password-hash".getBytes()) + .setPasswordSalt("salt".getBytes()) + .build()); + UserImportOptions options = UserImportOptions.withHash( + Pbkdf2Sha256.builder() + .setRounds(100000) + .build()); + UserImportResult result = FirebaseAuth.getInstance().importUsers(users, options); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + System.out.println("Error importing users: " + e.getMessage()); + } + // [END import_with_pbkdf] + } + + public void importWithStandardScrypt() { + // [START import_with_standard_scrypt] + try { + List users = Collections.singletonList(ImportUserRecord.builder() + .setUid("some-uid") + .setEmail("user@example.com") + .setPasswordHash("password-hash".getBytes()) + .setPasswordSalt("salt".getBytes()) + .build()); + UserImportOptions options = UserImportOptions.withHash( + StandardScrypt.builder() + .setMemoryCost(1024) + .setParallelization(16) + .setBlockSize(8) + .setDerivedKeyLength(64) + .build()); + UserImportResult result = FirebaseAuth.getInstance().importUsers(users, options); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + System.out.println("Error importing users: " + e.getMessage()); + } + // [END import_with_standard_scrypt] + } + + public void importWithBcrypt() { + // [START import_with_bcrypt] + try { + List users = Collections.singletonList(ImportUserRecord.builder() + .setUid("some-uid") + .setEmail("user@example.com") + .setPasswordHash("password-hash".getBytes()) + .setPasswordSalt("salt".getBytes()) + .build()); + UserImportOptions options = UserImportOptions.withHash(Bcrypt.getInstance()); + UserImportResult result = FirebaseAuth.getInstance().importUsers(users, options); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + System.out.println("Error importing users: " + e.getMessage()); + } + // [END import_with_bcrypt] + } + + public void importWithScrypt() { + // [START import_with_scrypt] + try { + List users = Collections.singletonList(ImportUserRecord.builder() + .setUid("some-uid") + .setEmail("user@example.com") + .setPasswordHash("password-hash".getBytes()) + .setPasswordSalt("salt".getBytes()) + .build()); + UserImportOptions options = UserImportOptions.withHash( + Scrypt.builder() + // All the parameters below can be obtained from the Firebase Console's "Users" + // section. Base64 encoded parameters must be decoded into raw bytes. + .setKey(BaseEncoding.base64().decode("base64-secret")) + .setSaltSeparator(BaseEncoding.base64().decode("base64-salt-separator")) + .setRounds(8) + .setMemoryCost(14) + .build()); + UserImportResult result = FirebaseAuth.getInstance().importUsers(users, options); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + System.out.println("Error importing users: " + e.getMessage()); + } + // [END import_with_scrypt] + } + + public void importWithoutPassword() { + // [START import_without_password] + try { + List users = Collections.singletonList(ImportUserRecord.builder() + .setUid("some-uid") + .setDisplayName("John Doe") + .setEmail("johndoe@gmail.com") + .setPhotoUrl("http://www.example.com/12345678/photo.png") + .setEmailVerified(true) + .setPhoneNumber("+11234567890") + .putCustomClaim("admin", true) // set this user as admin + .addUserProvider(UserProvider.builder() // user with Google provider + .setUid("google-uid") + .setEmail("johndoe@gmail.com") + .setDisplayName("John Doe") + .setPhotoUrl("http://www.example.com/12345678/photo.png") + .setProviderId("google.com") + .build()) + .build()); + UserImportResult result = FirebaseAuth.getInstance().importUsers(users); + for (ErrorInfo indexedError : result.getErrors()) { + System.out.println("Failed to import user: " + indexedError.getReason()); + } + } catch (FirebaseAuthException e) { + System.out.println("Error importing users: " + e.getMessage()); + } + // [END import_without_password] + } } From 5bad997eef67640a83fe71ba8f377999411db102 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 31 May 2018 11:05:30 -0700 Subject: [PATCH 002/441] Staged Release 6.2.0 (#182) * Updating CHANGELOG for 6.2.0 release. * [maven-release-plugin] prepare release v6.2.0 * [maven-release-plugin] prepare for next development iteration --- CHANGELOG.md | 4 ++++ pom.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26bb3ef41..e01aee042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.2.0 + - [added] Added new `importUsersAsync()` API for bulk importing users into Firebase Auth. diff --git a/pom.xml b/pom.xml index 32b90b01f..4b91112b4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.1.1-SNAPSHOT + 6.2.1-SNAPSHOT jar firebase-admin From d85c66aa9a9ffa5c6e315475990665b9f3cd158a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 26 Jun 2018 14:32:35 -0700 Subject: [PATCH 003/441] Reading project ID from GOOGLE_CLOUD_PROJECT (#189) * Reading project ID from GOOGLE_CLOUD_PROJECT * Updated changelog * Adding variable precendece test case --- CHANGELOG.md | 3 +- .../java/com/google/firebase/FirebaseApp.java | 3 + .../firebase/cloud/FirestoreClient.java | 8 +-- .../firebase/iid/FirebaseInstanceId.java | 2 +- .../firebase/messaging/FirebaseMessaging.java | 2 +- .../com/google/firebase/FirebaseAppTest.java | 56 +++++++++++++++++-- 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e01aee042..c7bf3e86e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [added] The SDK can now read the Firebase/GCP project ID from both + `GCLOUD_PROJECT` and `GOOGLE_CLOUD_PROJECT` environment variables. # v6.2.0 diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index 6c6710123..59a79e465 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -304,6 +304,9 @@ String getProjectId() { } // Try to get project ID from the environment. + if (Strings.isNullOrEmpty(projectId)) { + projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); + } if (Strings.isNullOrEmpty(projectId)) { projectId = System.getenv("GCLOUD_PROJECT"); } diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index 422b6d7de..210124972 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -19,9 +19,9 @@ *

A Google Cloud project ID is required to access Firestore. FirestoreClient determines the * project ID from the {@link com.google.firebase.FirebaseOptions} used to initialize the underlying * {@link FirebaseApp}. If that is not available, it examines the credentials used to initialize - * the app. Finally it attempts to get the project ID by looking up the GCLOUD_PROJECT environment - * variable. If a project ID cannot be determined by any of these methods, this API will throw - * a runtime exception. + * the app. Finally it attempts to get the project ID by looking up the GOOGLE_CLOUD_PROJECT and + * GCLOUD_PROJECT environment variables. If a project ID cannot be determined by any of these + * methods, this API will throw a runtime exception. */ public class FirestoreClient { @@ -33,7 +33,7 @@ private FirestoreClient(FirebaseApp app) { checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required for accessing Firestore. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " - + "set the project ID via the GCLOUD_PROJECT environment variable."); + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); this.firestore = FirestoreOptions.newBuilder() .setCredentials(ImplFirebaseTrampolines.getCredentials(app)) .setProjectId(projectId) diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index f3b6e5cb4..9a6d185bf 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -79,7 +79,7 @@ private FirebaseInstanceId(FirebaseApp app) { checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access instance ID service. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " - + "set the project ID via the GCLOUD_PROJECT environment variable."); + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); } /** diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 83bed681c..7ba1edd4d 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -106,7 +106,7 @@ private FirebaseMessaging(FirebaseApp app) { checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access messaging service. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " - + "set the project ID via the GCLOUD_PROJECT environment variable."); + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); this.url = String.format(FCM_URL, projectId); } diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index e549c0955..6cc0b7103 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -33,6 +33,8 @@ import com.google.auth.oauth2.OAuth2Credentials; import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener; import com.google.common.base.Defaults; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp.TokenRefresher; import com.google.firebase.FirebaseOptions.Builder; @@ -49,11 +51,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.BeforeClass; @@ -97,7 +99,7 @@ public void testGetInstancePersistedNotInitialized() { } @Test - public void testGetProjectIdFromOptions() throws Exception { + public void testGetProjectIdFromOptions() { FirebaseOptions options = new FirebaseOptions.Builder(OPTIONS) .setProjectId("explicit-project-id") .build(); @@ -107,12 +109,54 @@ public void testGetProjectIdFromOptions() throws Exception { } @Test - public void testGetProjectIdFromCredential() throws Exception { + public void testGetProjectIdFromCredential() { FirebaseApp app = FirebaseApp.initializeApp(OPTIONS, "myApp"); String projectId = ImplFirebaseTrampolines.getProjectId(app); assertEquals("mock-project-id", projectId); } + @Test + public void testGetProjectIdFromEnvironment() { + List variables = ImmutableList.of("GCLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT"); + for (String variable : variables) { + String gcloudProject = System.getenv(variable); + TestUtils.setEnvironmentVariables(ImmutableMap.of(variable, "project-id-1")); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials()) + .build(); + try { + FirebaseApp app = FirebaseApp.initializeApp(options,"myApp_" + variable); + String projectId = ImplFirebaseTrampolines.getProjectId(app); + assertEquals("project-id-1", projectId); + } finally { + TestUtils.setEnvironmentVariables(ImmutableMap.of( + variable, Strings.nullToEmpty(gcloudProject))); + } + } + } + + @Test + public void testProjectIdEnvironmentVariablePrecedence() { + Map currentValues = new HashMap<>(); + currentValues.put("GCLOUD_PROJECT", Strings.nullToEmpty( + System.getenv("GCLOUD_PROJECT"))); + currentValues.put("GOOGLE_CLOUD_PROJECT", Strings.nullToEmpty( + System.getenv("GOOGLE_CLOUD_PROJECT"))); + + TestUtils.setEnvironmentVariables(ImmutableMap.of( + "GCLOUD_PROJECT", "project-id-1", "GOOGLE_CLOUD_PROJECT", "project-id-2")); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials()) + .build(); + try { + FirebaseApp app = FirebaseApp.initializeApp(options,"myApp"); + String projectId = ImplFirebaseTrampolines.getProjectId(app); + assertEquals("project-id-2", projectId); + } finally { + TestUtils.setEnvironmentVariables(currentValues); + } + } + @Test(expected = IllegalStateException.class) public void testRehydratingDeletedInstanceThrows() { final String name = "myApp"; @@ -251,7 +295,7 @@ public void testApiInitForDefaultApp() { } @Test - public void testTokenCaching() throws ExecutionException, InterruptedException, IOException { + public void testTokenCaching() { FirebaseApp firebaseApp = FirebaseApp.initializeApp(getMockCredentialOptions(), "myApp"); String token1 = TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false); @@ -263,7 +307,7 @@ public void testTokenCaching() throws ExecutionException, InterruptedException, } @Test - public void testTokenForceRefresh() throws ExecutionException, InterruptedException, IOException { + public void testTokenForceRefresh() { FirebaseApp firebaseApp = FirebaseApp.initializeApp(getMockCredentialOptions(), "myApp"); String token1 = TestOnlyImplFirebaseTrampolines.getToken(firebaseApp, false); String token2 = TestOnlyImplFirebaseTrampolines.getToken(firebaseApp, true); @@ -618,7 +662,7 @@ TokenRefresher create(FirebaseApp app) { private static class MockGoogleCredentials extends GoogleCredentials { @Override - public AccessToken refreshAccessToken() throws IOException { + public AccessToken refreshAccessToken() { Date expiry = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); return new AccessToken(UUID.randomUUID().toString(), expiry); } From b5919dfd7a51bfd02f72fcd3d68415f06fbfb8e5 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 11 Jul 2018 14:40:02 -0700 Subject: [PATCH 004/441] Fixing an incorrect RTDB snippet (#192) --- .../com/google/firebase/snippets/FirebaseDatabaseSnippets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java index 2847ae123..baf4d3b69 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java @@ -145,7 +145,7 @@ public void savingData() { // [START adding_completion_callback] DatabaseReference dataRef = ref.child("data"); - dataRef.setValueAsync("I'm writing data", new DatabaseReference.CompletionListener() { + dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { if (databaseError != null) { From cd213e06fac08e20569a75ff27843e5ef82de5b7 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 12 Jul 2018 11:01:59 -0700 Subject: [PATCH 005/441] Support for creating custom tokens without service account credentials (#183) * Ability to sign tokens without a service account credential * Temp fixes for unit tests * Cleaned up the new JWT sign logic * Cleaned up the new JWT sign logic * Updated changelog * Moved token issuer to CyptoSigner interface * Modifications to the token sign protocol (go/firebase-admin-sign) * Caching the token factory after first call * Updated documentation * Merged with master * Updated API docs * Fixing style error * Updated error message * Updated documentation * Renamed serviceAccount to serviceAccountId * Fixed lint error * Added snippet; updated error message to sync with documentation * Minor API doc updates --- CHANGELOG.md | 4 + .../com/google/firebase/FirebaseOptions.java | 37 +++ .../google/firebase/auth/FirebaseAuth.java | 67 ++++-- .../firebase/auth/internal/CryptoSigner.java | 47 ++++ .../firebase/auth/internal/CryptoSigners.java | 170 ++++++++++++++ .../auth/internal/FirebaseTokenFactory.java | 73 +++--- .../google/firebase/auth/FirebaseAuthIT.java | 31 ++- .../firebase/auth/FirebaseAuthTest.java | 9 +- .../auth/internal/CryptoSignersTest.java | 212 ++++++++++++++++++ .../internal/FirebaseTokenFactoryTest.java | 75 ++++--- .../snippets/FirebaseAppSnippets.java | 9 + 11 files changed, 652 insertions(+), 82 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/internal/CryptoSigner.java create mode 100644 src/main/java/com/google/firebase/auth/internal/CryptoSigners.java create mode 100644 src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bf3e86e..da38156b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- [added] Implemented the ability to create custom tokens without + service account credentials. +- [added] Added the `setServiceAccount()` method to the + `FirebaseOptions.Builder` API. - [added] The SDK can now read the Firebase/GCP project ID from both `GCLOUD_PROJECT` and `GOOGLE_CLOUD_PROJECT` environment variables. diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index 0cdf87bf8..ae9b23370 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -60,6 +60,7 @@ public final class FirebaseOptions { private final GoogleCredentials credentials; private final Map databaseAuthVariableOverride; private final String projectId; + private final String serviceAccountId; private final HttpTransport httpTransport; private final int connectTimeout; private final int readTimeout; @@ -77,6 +78,11 @@ private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) { checkArgument(!builder.storageBucket.startsWith("gs://"), "StorageBucket must not include 'gs://' prefix."); } + if (!Strings.isNullOrEmpty(builder.serviceAccountId)) { + this.serviceAccountId = builder.serviceAccountId; + } else { + this.serviceAccountId = null; + } this.storageBucket = builder.storageBucket; this.httpTransport = checkNotNull(builder.httpTransport, "FirebaseOptions must be initialized with a non-null HttpTransport."); @@ -131,6 +137,16 @@ public String getProjectId() { return projectId; } + /** + * Returns the client email address of the service account. + * + * @return The client email of the service account set via + * {@link Builder#setServiceAccountId(String)} + */ + public String getServiceAccountId() { + return serviceAccountId; + } + /** * Returns the HttpTransport used to call remote HTTP endpoints. This transport is * used by all services of the SDK, except for FirebaseDatabase. @@ -192,6 +208,9 @@ public static final class Builder { @Key("storageBucket") private String storageBucket; + + @Key("serviceAccountId") + private String serviceAccountId; private GoogleCredentials credentials; private HttpTransport httpTransport = Utils.getDefaultTransport(); @@ -310,6 +329,24 @@ public Builder setProjectId(@NonNull String projectId) { return this; } + /** + * Sets the client email address of the service account that should be associated with an app. + * + *

This is used to + * create custom auth tokens when service account credentials are not available. The client + * email address of a service account can be found in the {@code client_email} field of the + * service account JSON. + * + * @param serviceAccountId A service account email address string. + * @return This Builder instance is returned so subsequent calls can be chained. + */ + public Builder setServiceAccountId(@NonNull String serviceAccountId) { + checkArgument(!Strings.isNullOrEmpty(serviceAccountId), + "Service account ID must not be null or empty"); + this.serviceAccountId = serviceAccountId; + return this; + } + /** * Sets the HttpTransport used to make remote HTTP calls. A reasonable default * is used if not explicitly set. The transport specified by calling this method is diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 47dd3ae6a..eb258952a 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -23,8 +23,6 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Clock; import com.google.api.core.ApiFuture; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.firebase.FirebaseApp; @@ -42,10 +40,10 @@ import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.io.IOException; -import java.security.GeneralSecurityException; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -65,10 +63,10 @@ public class FirebaseAuth { private final FirebaseApp firebaseApp; private final KeyManagers keyManagers; - private final GoogleCredentials credentials; private final String projectId; private final JsonFactory jsonFactory; private final FirebaseUserManager userManager; + private final AtomicReference tokenFactory; private final AtomicBoolean destroyed; private final Object lock; @@ -85,10 +83,10 @@ private FirebaseAuth(FirebaseApp firebaseApp) { this.firebaseApp = checkNotNull(firebaseApp); this.keyManagers = checkNotNull(keyManagers); this.clock = checkNotNull(clock); - this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp); this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp); this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); this.userManager = new FirebaseUserManager(firebaseApp); + this.tokenFactory = new AtomicReference<>(null); this.destroyed = new AtomicBoolean(false); this.lock = new Object(); } @@ -287,8 +285,21 @@ public String createCustomToken(@NonNull String uid) throws FirebaseAuthExceptio * signInWithCustomToken * authentication API. * - *

{@link FirebaseApp} must have been initialized with service account credentials to use - * call this method. + *

This method attempts to generate a token using: + *

    + *
  1. the private key of {@link FirebaseApp}'s service account credentials, if provided at + * initialization. + *
  2. the IAM service + * if a service account email was specified via + * {@link com.google.firebase.FirebaseOptions.Builder#setServiceAccountId(String)}. + *
  3. the App Identity + * service if the code is deployed in the Google App Engine standard environment. + *
  4. the + * local Metadata server if the code is deployed in a different GCP-managed environment + * like Google Compute Engine. + *
+ * + *

This method throws an exception when all the above fail. * * @param uid The UID to store in the token. This identifies the user to other Firebase services * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. @@ -296,8 +307,9 @@ public String createCustomToken(@NonNull String uid) throws FirebaseAuthExceptio * security rules in Database, Storage, etc.). These must be able to be serialized to JSON * (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.) * @return A Firebase custom token string. - * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not - * been initialized with service account credentials. + * @throws IllegalArgumentException If the specified uid is null or empty. + * @throws IllegalStateException If the SDK fails to discover a viable approach for signing + * tokens. * @throws FirebaseAuthException If an error occurs while generating the custom token. */ public String createCustomToken(@NonNull String uid, @@ -342,21 +354,13 @@ private CallableOperation createCustomTokenOp( final String uid, final Map developerClaims) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - checkArgument(credentials instanceof ServiceAccountCredentials, - "Must initialize FirebaseApp with a service account credential to call " - + "createCustomToken()"); + final FirebaseTokenFactory tokenFactory = ensureTokenFactory(); return new CallableOperation() { @Override public String execute() throws FirebaseAuthException { - final ServiceAccountCredentials serviceAccount = (ServiceAccountCredentials) credentials; - FirebaseTokenFactory tokenFactory = FirebaseTokenFactory.getInstance(); try { - return tokenFactory.createSignedCustomAuthTokenForUser( - uid, - developerClaims, - serviceAccount.getClientEmail(), - serviceAccount.getPrivateKey()); - } catch (GeneralSecurityException | IOException e) { + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); + } catch (IOException e) { throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN, "Failed to generate a custom token", e); } @@ -364,6 +368,29 @@ public String execute() throws FirebaseAuthException { }; } + private FirebaseTokenFactory ensureTokenFactory() { + FirebaseTokenFactory result = this.tokenFactory.get(); + if (result == null) { + synchronized (lock) { + result = this.tokenFactory.get(); + if (result == null) { + try { + result = FirebaseTokenFactory.fromApp(firebaseApp, clock); + this.tokenFactory.set(result); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK " + + "with service account credentials or specify a service account " + + "ID with iam.serviceAccounts.signBlob permission. Please refer to " + + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more " + + "details on creating custom tokens.", e); + } + } + } + } + return result; + } + /** * Parses and verifies a Firebase ID Token. * diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java new file mode 100644 index 000000000..3036f9f28 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.firebase.internal.NonNull; +import java.io.IOException; + +/** + * Represents an object that can be used to cryptographically sign data. Mainly used for signing + * custom JWT tokens issued to Firebase users. + * + *

See {@link com.google.firebase.auth.FirebaseAuth#createCustomToken(String)}. + */ +interface CryptoSigner { + + /** + * Signs the given payload. + * + * @param payload Data to be signed + * @return Signature as a byte array + * @throws IOException If an error occurs during signing + */ + @NonNull + byte[] sign(@NonNull byte[] payload) throws IOException; + + /** + * Returns the client email of the service account used to sign payloads. + * + * @return A service account client email + */ + @NonNull + String getAccount(); +} diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java new file mode 100644 index 000000000..3447f05d9 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -0,0 +1,170 @@ +package com.google.firebase.auth.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.util.Key; +import com.google.api.client.util.StringUtils; +import com.google.auth.ServiceAccountSigner; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.internal.NonNull; +import java.io.IOException; +import java.util.Map; + +/** + * A set of {@link CryptoSigner} implementations and utilities for interacting with them. + */ +class CryptoSigners { + + private static final String METADATA_SERVICE_URL = + "http://metadata/computeMetadata/v1/instance/service-accounts/default/email"; + + /** + * A {@link CryptoSigner} implementation that uses service account credentials or equivalent + * crypto-capable credentials for signing data. + */ + static class ServiceAccountCryptoSigner implements CryptoSigner { + + private final ServiceAccountSigner signer; + + ServiceAccountCryptoSigner(@NonNull ServiceAccountSigner signer) { + this.signer = checkNotNull(signer); + } + + @Override + public byte[] sign(byte[] payload) { + return signer.sign(payload); + } + + @Override + public String getAccount() { + return signer.getAccount(); + } + } + + /** + * @ {@link CryptoSigner} implementation that uses the + * + * Google IAM service to sign data. + */ + static class IAMCryptoSigner implements CryptoSigner { + + private static final String IAM_SIGN_BLOB_URL = + "https://iam.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; + + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final String serviceAccount; + private HttpResponseInterceptor interceptor; + + IAMCryptoSigner( + @NonNull HttpRequestFactory requestFactory, + @NonNull JsonFactory jsonFactory, + @NonNull String serviceAccount) { + this.requestFactory = checkNotNull(requestFactory); + this.jsonFactory = checkNotNull(jsonFactory); + checkArgument(!Strings.isNullOrEmpty(serviceAccount)); + this.serviceAccount = serviceAccount; + } + + void setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + } + + @Override + public byte[] sign(byte[] payload) throws IOException { + String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); + HttpResponse response = null; + String encodedPayload = BaseEncoding.base64().encode(payload); + Map content = ImmutableMap.of("bytesToSign", encodedPayload); + try { + HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(encodedUrl), + new JsonHttpContent(jsonFactory, content)); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); + response = request.execute(); + SignBlobResponse parsed = response.parseAs(SignBlobResponse.class); + return BaseEncoding.base64().decode(parsed.signature); + } finally { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // Ignored + } + } + } + } + + @Override + public String getAccount() { + return serviceAccount; + } + } + + public static class SignBlobResponse { + @Key("signature") + private String signature; + } + + /** + * Initializes a {@link CryptoSigner} instance for the given Firebase app. Follows the protocol + * documented at go/firebase-admin-sign. + */ + static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOException { + GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp); + + // If the SDK was initialized with a service account, use it to sign bytes. + if (credentials instanceof ServiceAccountCredentials) { + return new ServiceAccountCryptoSigner((ServiceAccountCredentials) credentials); + } + + FirebaseOptions options = firebaseApp.getOptions(); + HttpRequestFactory requestFactory = options.getHttpTransport().createRequestFactory( + new FirebaseRequestInitializer(firebaseApp)); + JsonFactory jsonFactory = options.getJsonFactory(); + + // If the SDK was initialized with a service account email, use it with the IAM service + // to sign bytes. + String serviceAccountId = options.getServiceAccountId(); + if (!Strings.isNullOrEmpty(serviceAccountId)) { + return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + } + + // If the SDK was initialized with some other credential type that supports signing + // (e.g. GAE credentials), use it to sign bytes. + if (credentials instanceof ServiceAccountSigner) { + return new ServiceAccountCryptoSigner((ServiceAccountSigner) credentials); + } + + // Attempt to discover a service account email from the local Metadata service. Use it + // with the IAM service to sign bytes. + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(METADATA_SERVICE_URL)); + request.getHeaders().set("Metadata-Flavor", "Google"); + HttpResponse response = request.execute(); + try { + byte[] output = ByteStreams.toByteArray(response.getContent()); + serviceAccountId = StringUtils.newStringUtf8(output).trim(); + return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + } finally { + response.disconnect(); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java index f9d7233c2..72ef91791 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -16,64 +16,56 @@ package com.google.firebase.auth.internal; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; -import com.google.common.base.Preconditions; +import com.google.api.client.util.StringUtils; +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.PrivateKey; import java.util.Collection; import java.util.Map; /** - * Provides helper methods to simplify the creation of FirebaseCustomAuthTokens. + * Provides helper methods to simplify the creation of Firebase custom auth tokens. * *

This class is designed to hide underlying implementation details from a Firebase developer. */ public class FirebaseTokenFactory { - private static FirebaseTokenFactory instance; - - private JsonFactory factory; - private Clock clock; + private final JsonFactory jsonFactory; + private final Clock clock; + private final CryptoSigner signer; - public FirebaseTokenFactory(JsonFactory factory, Clock clock) { - this.factory = factory; - this.clock = clock; + FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + this.jsonFactory = checkNotNull(jsonFactory); + this.clock = checkNotNull(clock); + this.signer = checkNotNull(signer); } - public static FirebaseTokenFactory getInstance() { - if (null == instance) { - instance = new FirebaseTokenFactory(new GsonFactory(), Clock.SYSTEM); - } - - return instance; - } - - public String createSignedCustomAuthTokenForUser(String uid, String issuer, PrivateKey privateKey) - throws GeneralSecurityException, IOException { - return createSignedCustomAuthTokenForUser(uid, null, issuer, privateKey); + String createSignedCustomAuthTokenForUser(String uid) throws IOException { + return createSignedCustomAuthTokenForUser(uid, null); } public String createSignedCustomAuthTokenForUser( - String uid, Map developerClaims, String issuer, PrivateKey privateKey) - throws GeneralSecurityException, IOException { - Preconditions.checkState(uid != null, "Uid must be provided."); - Preconditions.checkState(issuer != null && !"".equals(issuer), "Must provide an issuer."); - Preconditions.checkState(uid.length() <= 128, "Uid must be shorter than 128 characters."); + String uid, Map developerClaims) throws IOException { + checkArgument(!Strings.isNullOrEmpty(uid), "Uid must be provided."); + checkArgument(uid.length() <= 128, "Uid must be shorter than 128 characters."); JsonWebSignature.Header header = new JsonWebSignature.Header().setAlgorithm("RS256"); - long issuedAt = clock.currentTimeMillis() / 1000; + final long issuedAt = clock.currentTimeMillis() / 1000; FirebaseCustomAuthToken.Payload payload = new FirebaseCustomAuthToken.Payload() .setUid(uid) - .setIssuer(issuer) - .setSubject(issuer) + .setIssuer(signer.getAccount()) + .setSubject(signer.getAccount()) .setAudience(FirebaseCustomAuthToken.FIREBASE_AUDIENCE) .setIssuedAtTimeSeconds(issuedAt) .setExpirationTimeSeconds(issuedAt + FirebaseCustomAuthToken.TOKEN_DURATION_SECONDS); @@ -90,7 +82,24 @@ public String createSignedCustomAuthTokenForUser( jsonObject.putAll(developerClaims); payload.setDeveloperClaims(jsonObject); } + return signPayload(header, payload); + } + + private String signPayload(JsonWebSignature.Header header, + FirebaseCustomAuthToken.Payload payload) throws IOException { + String headerString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)); + String payloadString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + String content = headerString + "." + payloadString; + byte[] contentBytes = StringUtils.getBytesUtf8(content); + String signature = Base64.encodeBase64URLSafeString(signer.sign(contentBytes)); + return content + "." + signature; + } - return JsonWebSignature.signUsingRsaSha256(privateKey, factory, header, payload); + public static FirebaseTokenFactory fromApp( + FirebaseApp firebaseApp, Clock clock) throws IOException { + return new FirebaseTokenFactory( + firebaseApp.getOptions().getJsonFactory(), + clock, + CryptoSigners.getCryptoSigner(firebaseApp)); } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 99171d809..54c225960 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -35,11 +35,16 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; +import com.google.auth.ServiceAccountSigner; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.hash.Scrypt; @@ -364,6 +369,31 @@ public void testCustomToken() throws Exception { assertEquals("user1", decoded.getUid()); } + @Test + public void testCustomTokenWithIAM() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(masterApp); + AccessToken token = credentials.getAccessToken(); + if (token == null) { + token = credentials.refreshAccessToken(); + } + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.of(token)) + .setServiceAccountId(((ServiceAccountSigner) credentials).getAccount()) + .setProjectId(IntegrationTestUtils.getProjectId()) + .build(); + FirebaseApp customApp = FirebaseApp.initializeApp(options, "tempApp"); + try { + FirebaseAuth auth = FirebaseAuth.getInstance(customApp); + String customToken = auth.createCustomTokenAsync("user1").get(); + String idToken = signInWithCustomToken(customToken); + FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); + assertEquals("user1", decoded.getUid()); + } finally { + customApp.delete(); + } + } + @Test public void testVerifyIdToken() throws Exception { String customToken = auth.createCustomTokenAsync("user2").get(); @@ -541,5 +571,4 @@ private void checkRecreate(String uid) throws Exception { assertEquals("uid-already-exists", ((FirebaseAuthException) e.getCause()).getErrorCode()); } } - } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index bb430d285..1a4ccd4af 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -301,10 +301,13 @@ public void testServiceAccountRequired() throws Exception { try { FirebaseAuth.getInstance(app).createCustomTokenAsync("foo").get(); fail("Expected exception."); - } catch (IllegalArgumentException expected) { + } catch (IllegalStateException expected) { Assert.assertEquals( - "Must initialize FirebaseApp with a service account credential to call " - + "createCustomToken()", + "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK with " + + "service account credentials or specify a service account ID with " + + "iam.serviceAccounts.signBlob permission. Please refer to " + + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details " + + "on creating custom tokens.", expected.getMessage()); } } diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java new file mode 100644 index 000000000..bc98add3c --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.auth.ServiceAccountSigner; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestResponseInterceptor; +import java.io.IOException; +import org.junit.After; +import org.junit.Test; + +public class CryptoSignersTest { + + @Test + public void testServiceAccountCryptoSigner() throws IOException { + ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( + ServiceAccount.EDITOR.asStream()); + byte[] expected = credentials.sign("foo".getBytes()); + CryptoSigner signer = new CryptoSigners.ServiceAccountCryptoSigner(credentials); + byte[] data = signer.sign("foo".getBytes()); + assertArrayEquals(expected, data); + } + + @Test + public void testInvalidServiceAccountCryptoSigner() { + try { + new CryptoSigners.ServiceAccountCryptoSigner(null); + fail("No error thrown for null service account signer"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testIAMCryptoSigner() throws IOException { + String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); + String response = Utils.getDefaultJsonFactory().toString( + ImmutableMap.of("signature", signature)); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response)) + .build(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + CryptoSigners.IAMCryptoSigner signer = new CryptoSigners.IAMCryptoSigner( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + "test-service-account@iam.gserviceaccount.com"); + signer.setInterceptor(interceptor); + + byte[] data = signer.sign("foo".getBytes()); + assertArrayEquals("signed-bytes".getBytes(), data); + final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + + "test-service-account@iam.gserviceaccount.com:signBlob"; + assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + } + + @Test + public void testInvalidIAMCryptoSigner() { + try { + new CryptoSigners.IAMCryptoSigner(null, Utils.getDefaultJsonFactory(), "test"); + fail("No error thrown for null request factory"); + } catch (NullPointerException expected) { + // expected + } + + MockHttpTransport transport = new MockHttpTransport(); + try { + new CryptoSigners.IAMCryptoSigner(transport.createRequestFactory(), null, "test"); + fail("No error thrown for null json factory"); + } catch (NullPointerException expected) { + // expected + } + + try { + new CryptoSigners.IAMCryptoSigner(transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), null); + fail("No error thrown for null service account"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new CryptoSigners.IAMCryptoSigner(transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), ""); + fail("No error thrown for empty service account"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testMetadataService() throws IOException { + String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); + String response = Utils.getDefaultJsonFactory().toString( + ImmutableMap.of("signature", signature)); + MockHttpTransport transport = new MultiRequestMockHttpTransport( + ImmutableList.of( + new MockLowLevelHttpResponse().setContent("metadata-server@iam.gserviceaccount.com"), + new MockLowLevelHttpResponse().setContent(response))); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + CryptoSigner signer = CryptoSigners.getCryptoSigner(app); + + assertTrue(signer instanceof CryptoSigners.IAMCryptoSigner); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ((CryptoSigners.IAMCryptoSigner) signer).setInterceptor(interceptor); + + byte[] data = signer.sign("foo".getBytes()); + assertArrayEquals("signed-bytes".getBytes(), data); + final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + + "metadata-server@iam.gserviceaccount.com:signBlob"; + assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + } + + @Test + public void testExplicitServiceAccountEmail() throws IOException { + String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); + String response = Utils.getDefaultJsonFactory().toString( + ImmutableMap.of("signature", signature)); + + // Explicit service account should get precedence + MockHttpTransport transport = new MultiRequestMockHttpTransport( + ImmutableList.of( + new MockLowLevelHttpResponse().setContent(response))); + FirebaseOptions options = new FirebaseOptions.Builder() + .setServiceAccountId("explicit-service-account@iam.gserviceaccount.com") + .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + CryptoSigner signer = CryptoSigners.getCryptoSigner(app); + assertTrue(signer instanceof CryptoSigners.IAMCryptoSigner); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ((CryptoSigners.IAMCryptoSigner) signer).setInterceptor(interceptor); + + byte[] data = signer.sign("foo".getBytes()); + assertArrayEquals("signed-bytes".getBytes(), data); + final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + + "explicit-service-account@iam.gserviceaccount.com:signBlob"; + assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + } + + @Test + public void testCredentialsWithSigner() throws IOException { + // Should fall back to signing-enabled credential + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options, "customApp"); + CryptoSigner signer = CryptoSigners.getCryptoSigner(app); + assertTrue(signer instanceof CryptoSigners.ServiceAccountCryptoSigner); + assertEquals("credential-signer@iam.gserviceaccount.com", signer.getAccount()); + byte[] data = signer.sign("foo".getBytes()); + assertArrayEquals("local-signed-bytes".getBytes(), data); + } + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + private static class MockGoogleCredentialsWithSigner extends MockGoogleCredentials implements + ServiceAccountSigner { + + MockGoogleCredentialsWithSigner(String tokenValue) { + super(tokenValue); + } + + @Override + public String getAccount() { + return "credential-signer@iam.gserviceaccount.com"; + } + + @Override + public byte[] sign(byte[] toSign) { + return "local-signed-bytes".getBytes(); + } + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java index 501ddf75d..71b95bee8 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -16,6 +16,7 @@ package com.google.firebase.auth.internal; +import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -23,12 +24,16 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.testing.http.FixedClock; +import com.google.api.client.util.SecurityUtils; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.PrivateKey; import java.util.Map; import org.junit.Rule; import org.junit.Test; @@ -55,19 +60,25 @@ public void checkSignatureForToken() throws Exception { FixedClock clock = new FixedClock(2002L); - FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock); - - String jwt = - tokenFactory.createSignedCustomAuthTokenForUser( - USER_ID, EXTRA_CLAIMS, ISSUER, keys.getPrivate()); + FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock, + new TestCryptoSigner(keys.getPrivate())); + String jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID, EXTRA_CLAIMS); FirebaseCustomAuthToken signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); assertEquals("RS256", signedJwt.getHeader().getAlgorithm()); assertEquals(ISSUER, signedJwt.getPayload().getIssuer()); assertEquals(ISSUER, signedJwt.getPayload().getSubject()); assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); + assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); + signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); + assertEquals("RS256", signedJwt.getHeader().getAlgorithm()); + assertEquals(ISSUER, signedJwt.getPayload().getIssuer()); + assertEquals(ISSUER, signedJwt.getPayload().getSubject()); + assertEquals(USER_ID, signedJwt.getPayload().getUid()); + assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); } @@ -79,10 +90,11 @@ public void failsWhenUidIsNull() throws Exception { FixedClock clock = new FixedClock(2002L); - FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock); + FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock, + new TestCryptoSigner(keys.getPrivate())); - thrown.expect(IllegalStateException.class); - tokenFactory.createSignedCustomAuthTokenForUser(null, ISSUER, keys.getPrivate()); + thrown.expect(IllegalArgumentException.class); + tokenFactory.createSignedCustomAuthTokenForUser(null); } @Test @@ -93,40 +105,51 @@ public void failsWhenUidIsTooLong() throws Exception { FixedClock clock = new FixedClock(2002L); - FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock); + FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock, + new TestCryptoSigner(keys.getPrivate())); - thrown.expect(IllegalStateException.class); + thrown.expect(IllegalArgumentException.class); tokenFactory.createSignedCustomAuthTokenForUser( - Strings.repeat("a", 129), ISSUER, keys.getPrivate()); + Strings.repeat("a", 129)); } @Test - public void failsWhenIssuerIsNull() throws Exception { + public void failsWhenExtraClaimsContainsReservedKey() throws Exception { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(512); KeyPair keys = keyGen.genKeyPair(); FixedClock clock = new FixedClock(2002L); - FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock); + FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock, + new TestCryptoSigner(keys.getPrivate())); - thrown.expect(IllegalStateException.class); - tokenFactory.createSignedCustomAuthTokenForUser(USER_ID, null, keys.getPrivate()); + Map extraClaims = ImmutableMap.of("iss", "repeat issuer"); + thrown.expect(IllegalArgumentException.class); + tokenFactory.createSignedCustomAuthTokenForUser(USER_ID, extraClaims); } - @Test - public void failsWhenExtraClaimsContainsReservedKey() throws Exception { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); - keyGen.initialize(512); - KeyPair keys = keyGen.genKeyPair(); + private static class TestCryptoSigner implements CryptoSigner { - FixedClock clock = new FixedClock(2002L); + private final PrivateKey privateKey; - FirebaseTokenFactory tokenFactory = new FirebaseTokenFactory(FACTORY, clock); + TestCryptoSigner(PrivateKey privateKey) { + this.privateKey = checkNotNull(privateKey); + } - Map extraClaims = ImmutableMap.of("iss", "repeat issuer"); - thrown.expect(IllegalArgumentException.class); - tokenFactory.createSignedCustomAuthTokenForUser( - USER_ID, extraClaims, ISSUER, keys.getPrivate()); + @Override + public byte[] sign(byte[] payload) throws IOException { + try { + return SecurityUtils.sign(SecurityUtils.getSha256WithRsaSignatureAlgorithm(), + privateKey, payload); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + + @Override + public String getAccount() { + return ISSUER; + } } } diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java index 3713164ab..f4b76e752 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java @@ -120,4 +120,13 @@ public void initializeCustomApp() throws Exception { FirebaseDatabase otherDatabase = FirebaseDatabase.getInstance(otherApp); // [END access_services_nondefault] } + + public void initializeWithServiceAccountId() { + // [START initialize_sdk_with_service_account_id] + FirebaseOptions options = new FirebaseOptions.Builder() + .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com") + .build(); + FirebaseApp.initializeApp(options); + // [END initialize_sdk_with_service_account_id] + } } From 0a1c65209a3324d56fadb8f60ccd86e539cd637d Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 26 Jul 2018 14:35:43 -0700 Subject: [PATCH 006/441] Staged Release 6.3.0 (#194) * Updating CHANGELOG for 6.3.0 release. * [maven-release-plugin] prepare release v6.3.0 * [maven-release-plugin] prepare for next development iteration --- CHANGELOG.md | 4 ++++ pom.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da38156b4..fc14e4c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.3.0 + - [added] Implemented the ability to create custom tokens without service account credentials. - [added] Added the `setServiceAccount()` method to the diff --git a/pom.xml b/pom.xml index 4b91112b4..c7b368d90 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.2.1-SNAPSHOT + 6.3.1-SNAPSHOT jar firebase-admin From 78c61988a21759a9bcbfd5df66edb56c2e0b6921 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 30 Jul 2018 11:22:01 -0700 Subject: [PATCH 007/441] Added WebpushNotification.Builder API with additional Webpush parameters (#193) * Added webpush notification fields * Added documentation * Support for adding arbitrary key-value pairs to WebpushNotification * Updated changelog * Added a couple of helper methods for clarity * Updated comment * Renamed setTimestamp to setTimestampMillis as per API review feedback --- CHANGELOG.md | 3 +- .../firebase/messaging/WebpushConfig.java | 4 +- .../messaging/WebpushNotification.java | 368 +++++++++++++++++- .../messaging/FirebaseMessagingTest.java | 83 +++- 4 files changed, 442 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc14e4c7d..faa141eaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [added] `WebpushNotification` type now supports arbitrary key-value + pairs in its payload. # v6.3.0 diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java index 6b5ac4ac2..7c99580c6 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushConfig.java +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -35,12 +35,12 @@ public class WebpushConfig { private final Map data; @Key("notification") - private final WebpushNotification notification; + private final Map notification; private WebpushConfig(Builder builder) { this.headers = builder.headers.isEmpty() ? null : ImmutableMap.copyOf(builder.headers); this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); - this.notification = builder.notification; + this.notification = builder.notification != null ? builder.notification.getFields() : null; } /** diff --git a/src/main/java/com/google/firebase/messaging/WebpushNotification.java b/src/main/java/com/google/firebase/messaging/WebpushNotification.java index 92876c562..ef9b97f22 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushNotification.java +++ b/src/main/java/com/google/firebase/messaging/WebpushNotification.java @@ -16,23 +16,28 @@ package com.google.firebase.messaging; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Represents the Webpush-specific notification options that can be included in a {@link Message}. - * Instances of this class are thread-safe and immutable. + * Instances of this class are thread-safe and immutable. Supports most standard options defined + * in the Web + * Notification specification. */ public class WebpushNotification { - @Key("title") - private final String title; - - @Key("body") - private final String body; - - @Key("icon") - private final String icon; + private final Map fields; /** * Creates a new notification with the given title and body. Overrides the options set via @@ -54,8 +59,347 @@ public WebpushNotification(String title, String body) { * @param icon URL to the notifications icon. */ public WebpushNotification(String title, String body, @Nullable String icon) { - this.title = title; - this.body = body; - this.icon = icon; + this(builder().setTitle(title).setBody(body).setIcon(icon)); + } + + private WebpushNotification(Builder builder) { + ImmutableMap.Builder fields = ImmutableMap.builder(); + if (!builder.actions.isEmpty()) { + fields.put("actions", ImmutableList.copyOf(builder.actions)); + } + addNonNullNonEmpty(fields, "badge", builder.badge); + addNonNullNonEmpty(fields, "body", builder.body); + addNonNull(fields, "data", builder.data); + addNonNullNonEmpty(fields, "dir", builder.direction != null ? builder.direction.value : null); + addNonNullNonEmpty(fields, "icon", builder.icon); + addNonNullNonEmpty(fields, "image", builder.image); + addNonNullNonEmpty(fields, "lang", builder.language); + addNonNull(fields, "renotify", builder.renotify); + addNonNull(fields, "requireInteraction", builder.requireInteraction); + addNonNull(fields, "silent", builder.silent); + addNonNullNonEmpty(fields, "tag", builder.tag); + addNonNull(fields, "timestamp", builder.timestampMillis); + addNonNullNonEmpty(fields, "title", builder.title); + addNonNull(fields, "vibrate", builder.vibrate); + fields.putAll(builder.customData); + this.fields = fields.build(); + } + + Map getFields() { + return fields; + } + + /** + * Creates a new {@link WebpushNotification.Builder}. + * + * @return A {@link WebpushNotification.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Different directions a notification can be displayed in. + */ + public enum Direction { + AUTO("auto"), + LEFT_TO_RIGHT("ltr"), + RIGHT_TO_LEFT("rtl"); + + final String value; + + Direction(String value) { + this.value = value; + } + } + + /** + * Represents an action available to the users when the notification is presented. + */ + public static class Action { + @Key("action") + private final String action; + + @Key("title") + private final String title; + + @Key("icon") + private final String icon; + + /** + * Creates a new Action with the given action string and title. + * + * @param action Action string. + * @param title Title text. + */ + public Action(String action, String title) { + this(action, title, null); + } + + /** + * Creates a new Action with the given action string, title and icon URL. + * + * @param action Action string. + * @param title Title text. + * @param icon Icon URL or null. + */ + public Action(String action, String title, @Nullable String icon) { + checkArgument(!Strings.isNullOrEmpty(action)); + checkArgument(!Strings.isNullOrEmpty(title)); + this.action = action; + this.title = title; + this.icon = icon; + } + } + + public static class Builder { + + private final List actions = new ArrayList<>(); + private String badge; + private String body; + private Object data; + private Direction direction; + private String icon; + private String image; + private String language; + private Boolean renotify; + private Boolean requireInteraction; + private Boolean silent; + private String tag; + private Long timestampMillis; + private String title; + private List vibrate; + private final Map customData = new HashMap<>(); + + private Builder() {} + + /** + * Adds a notification action to the notification. + * + * @param action A non-null {@link Action}. + * @return This builder. + */ + public Builder addAction(@NonNull Action action) { + this.actions.add(action); + return this; + } + + /** + * Adds all the actions in the given list to the notification. + * + * @param actions A non-null list of actions. + * @return This builder. + */ + public Builder addAllActions(@NonNull List actions) { + this.actions.addAll(actions); + return this; + } + + /** + * Sets the URL of the image used to represent the notification when there is + * not enough space to display the notification itself. + * + * @param badge Badge URL. + * @return This builder. + */ + public Builder setBadge(String badge) { + this.badge = badge; + return this; + } + + /** + * Sets the body text of the notification. + * + * @param body Body text. + * @return This builder. + */ + public Builder setBody(String body) { + this.body = body; + return this; + } + + /** + * Sets any arbitrary data that should be associated with the notification. + * + * @param data A JSON-serializable object. + * @return This builder. + */ + public Builder setData(Object data) { + this.data = data; + return this; + } + + /** + * Sets the direction in which to display the notification. + * + * @param direction Direction enum value. + * @return This builder. + */ + public Builder setDirection(Direction direction) { + this.direction = direction; + return this; + } + + /** + * Sets the URL to the icon of the notification. + * + * @param icon Icon URL. + * @return This builder. + */ + public Builder setIcon(String icon) { + this.icon = icon; + return this; + } + + /** + * Sets the URL of an image to be displayed in the notification. + * + * @param image Image URL + * @return This builder. + */ + public Builder setImage(String image) { + this.image = image; + return this; + } + + /** + * Sets the language of the notification. + * + * @param language Notification language. + * @return This builder. + */ + public Builder setLanguage(String language) { + this.language = language; + return this; + } + + /** + * Sets whether the user should be notified after a new notification replaces an old one. + * + * @param renotify true to notify the user on replacement. + * @return This builder. + */ + public Builder setRenotify(boolean renotify) { + this.renotify = renotify; + return this; + } + + /** + * Sets whether a notification should remain active until the user clicks or dismisses it, + * rather than closing automatically. + * + * @param requireInteraction true to keep the notification active until user interaction. + * @return This builder. + */ + public Builder setRequireInteraction(boolean requireInteraction) { + this.requireInteraction = requireInteraction; + return this; + } + + /** + * Sets whether the notification should be silent. + * + * @param silent true to indicate that the notification should be silent. + * @return This builder. + */ + public Builder setSilent(boolean silent) { + this.silent = silent; + return this; + } + + /** + * Sets an identifying tag on the notification. + * + * @param tag A tag to be associated with the notification. + * @return This builder. + */ + public Builder setTag(String tag) { + this.tag = tag; + return this; + } + + /** + * Sets a timestamp value in milliseconds on the notification. + * + * @param timestampMillis A timestamp value as a number. + * @return This builder. + */ + public Builder setTimestampMillis(long timestampMillis) { + this.timestampMillis = timestampMillis; + return this; + } + + /** + * Sets the title text of the notification. + * + * @param title Title text. + * @return This builder. + */ + public Builder setTitle(String title) { + this.title = title; + return this; + } + + /** + * Sets a vibration pattern for the device's vibration hardware to emit + * when the notification fires. + * + * @param pattern An integer array representing a vibration pattern. + * @return This builder. + */ + public Builder setVibrate(int[] pattern) { + List list = new ArrayList<>(); + for (int value : pattern) { + list.add(value); + } + this.vibrate = ImmutableList.copyOf(list); + return this; + } + + /** + * Puts a custom key-value pair to the notification. + * + * @param key A non-null key. + * @param value A non-null, json-serializable value. + * @return This builder. + */ + public Builder putCustomData(@NonNull String key, @NonNull Object value) { + this.customData.put(key, value); + return this; + } + + /** + * Puts all the key-value pairs in the specified map to the notification. + * + * @param fields A non-null map. Map must not contain null keys or values. + * @return This builder. + */ + public Builder putAllCustomData(@NonNull Map fields) { + this.customData.putAll(fields); + return this; + } + + /** + * Creates a new {@link WebpushNotification} from the parameters set on this builder. + * + * @return A new {@link WebpushNotification} instance. + */ + public WebpushNotification build() { + return new WebpushNotification(this); + } + } + + private static void addNonNull( + ImmutableMap.Builder fields, String key, Object value) { + if (value != null) { + fields.put(key, value); + } + } + + private static void addNonNullNonEmpty( + ImmutableMap.Builder fields, String key, String value) { + if (!Strings.isNullOrEmpty(value)) { + fields.put(key, value); + } } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index d36db08c0..b269978f2 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -35,6 +35,8 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.messaging.WebpushNotification.Action; +import com.google.firebase.messaging.WebpushNotification.Direction; import com.google.firebase.testing.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; import java.io.ByteArrayOutputStream; @@ -657,7 +659,25 @@ private static Map> buildTestMessages() { "title", "test-title", "body", "test-body")))) )); - // Webpush message + // Webpush message (no notification) + builder.put( + Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "webpush", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3")) + )); + + // Webpush message (simple notification) builder.put( Message.builder() .setWebpushConfig(WebpushConfig.builder() @@ -678,6 +698,67 @@ private static Map> buildTestMessages() { "title", "test-title", "body", "test-body", "icon", "test-icon")) )); + // Webpush message (all fields) + builder.put( + Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(WebpushNotification.builder() + .setTitle("test-title") + .setBody("test-body") + .setIcon("test-icon") + .setBadge("test-badge") + .setImage("test-image") + .setLanguage("test-lang") + .setTag("test-tag") + .setData(ImmutableList.of("arbitrary", "data")) + .setDirection(Direction.AUTO) + .setRenotify(true) + .setRequireInteraction(false) + .setSilent(true) + .setTimestampMillis(100L) + .setVibrate(new int[]{200, 100, 200}) + .addAction(new Action("action1", "title1")) + .addAllActions(ImmutableList.of(new Action("action2", "title2", "icon2"))) + .putCustomData("k4", "v4") + .putAllCustomData(ImmutableMap.of("k5", "v5", "k6", "v6")) + .build()) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "webpush", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "notification", ImmutableMap.builder() + .put("title", "test-title") + .put("body", "test-body") + .put("icon", "test-icon") + .put("badge", "test-badge") + .put("image", "test-image") + .put("lang", "test-lang") + .put("tag", "test-tag") + .put("data", ImmutableList.of("arbitrary", "data")) + .put("renotify", true) + .put("requireInteraction", false) + .put("silent", true) + .put("dir", "auto") + .put("timestamp", new BigDecimal(100)) + .put("vibrate", ImmutableList.of( + new BigDecimal(200), new BigDecimal(100), new BigDecimal(200))) + .put("actions", ImmutableList.of( + ImmutableMap.of("action", "action1", "title", "title1"), + ImmutableMap.of("action", "action2", "title", "title2", "icon", "icon2"))) + .put("k4", "v4") + .put("k5", "v5") + .put("k6", "v6") + .build()) + )); + return builder.build(); } } From 0cf01057fa1a19381993a6ac480911e113162c75 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 15 Aug 2018 14:56:19 -0700 Subject: [PATCH 008/441] Basic firebaseopensource config (#197) --- .opensource/project.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .opensource/project.json diff --git a/.opensource/project.json b/.opensource/project.json new file mode 100644 index 000000000..74e68f048 --- /dev/null +++ b/.opensource/project.json @@ -0,0 +1,14 @@ +{ + "name": "Firebase Admin SDK - Java", + "platforms": [ + "Java", + "Admin" + ], + "content": "README.md", + "pages": [], + "related": [ + "firebase/firebase-admin-go", + "firebase/firebase-admin-node", + "firebase/firebase-admin-python" + ] +} From b79430e8afd951d6e2c6a678cd78f6dcb1a532c7 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 28 Aug 2018 11:14:23 -0700 Subject: [PATCH 009/441] Updating the API docs for IID (#198) --- .../java/com/google/firebase/iid/FirebaseInstanceId.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 9a6d185bf..7ac527778 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -111,10 +111,12 @@ void setInterceptor(HttpResponseInterceptor interceptor) { } /** - * Deletes the specified instance ID from Firebase. + * Deletes the specified instance ID and the associated data from Firebase. * - *

This can be used to delete an instance ID and associated user data from a Firebase project, - * pursuant to the General Data Protection Regulation (GDPR). + *

Note that Google Analytics for Firebase uses its own form of Instance ID to keep track of + * analytics data. Therefore deleting a regular Instance ID does not delete Analytics data. + * See + * Delete an Instance ID for more information. * * @param instanceId A non-null, non-empty instance ID string. * @throws IllegalArgumentException If the instance ID is null or empty. From 6da34c2f8cef951078a89d046d221d43e103dd6e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 28 Aug 2018 12:55:54 -0700 Subject: [PATCH 010/441] Staging the Release 6.4.0 (#199) * Updating CHANGELOG for 6.4.0 release. * [maven-release-plugin] prepare release v6.4.0 * [maven-release-plugin] prepare for next development iteration --- CHANGELOG.md | 4 ++++ pom.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faa141eaf..27071e330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.4.0 + - [added] `WebpushNotification` type now supports arbitrary key-value pairs in its payload. diff --git a/pom.xml b/pom.xml index c7b368d90..13b51555c 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.3.1-SNAPSHOT + 6.4.1-SNAPSHOT jar firebase-admin From 7a58ee066e94c79c3390dea44a59d16fb3bb4b97 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 28 Aug 2018 13:57:47 -0700 Subject: [PATCH 011/441] Minor edit suggested by the docs team (#200) --- .../java/com/google/firebase/messaging/WebpushNotification.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/messaging/WebpushNotification.java b/src/main/java/com/google/firebase/messaging/WebpushNotification.java index ef9b97f22..d17380543 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushNotification.java +++ b/src/main/java/com/google/firebase/messaging/WebpushNotification.java @@ -114,7 +114,7 @@ public enum Direction { } /** - * Represents an action available to the users when the notification is presented. + * Represents an action available to users when the notification is presented. */ public static class Action { @Key("action") From 469effa2b240bdcd978e65bf8f64a40155356811 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 13 Sep 2018 11:19:42 -0700 Subject: [PATCH 012/441] Upgrading Google Cloud Dependencies (#203) * Upgrading Google Cloud Dependencies * Exposing FirestoreOptions via FirebaseOptions * Updated comment --- pom.xml | 14 ++--- .../com/google/firebase/FirebaseOptions.java | 34 +++++++++++ .../firebase/ImplFirebaseTrampolines.java | 5 ++ .../firebase/cloud/FirestoreClient.java | 10 +++- .../google/firebase/FirebaseOptionsTest.java | 9 +++ .../firebase/auth/FirebaseAuthTest.java | 4 +- .../firebase/cloud/FirestoreClientTest.java | 56 +++++++++++++++++++ .../testing/IntegrationTestUtils.java | 7 ++- .../google/firebase/testing/TestUtils.java | 3 +- 9 files changed, 130 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 13b51555c..b82e5fc95 100644 --- a/pom.xml +++ b/pom.xml @@ -375,7 +375,7 @@ com.google.api-client google-api-client - 1.23.0 + 1.25.0 com.google.guava @@ -386,32 +386,32 @@ com.google.api-client google-api-client-gson - 1.23.0 + 1.25.0 com.google.http-client google-http-client - 1.23.0 + 1.25.0 com.google.api api-common - 1.2.0 + 1.7.0 com.google.auth google-auth-library-oauth2-http - 0.8.0 + 0.11.0 com.google.cloud google-cloud-storage - 1.27.0 + 1.43.0 com.google.cloud google-cloud-firestore - 0.45.0-beta + 0.61.0-beta diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index ae9b23370..99b4e1c5a 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -24,6 +24,7 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Key; import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.firestore.FirestoreOptions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.firebase.internal.FirebaseThreadManagers; @@ -66,6 +67,7 @@ public final class FirebaseOptions { private final int readTimeout; private final JsonFactory jsonFactory; private final ThreadManager threadManager; + private final FirestoreOptions firestoreOptions; private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) { this.credentials = checkNotNull(builder.credentials, @@ -94,6 +96,7 @@ private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) { this.connectTimeout = builder.connectTimeout; checkArgument(builder.readTimeout >= 0); this.readTimeout = builder.readTimeout; + this.firestoreOptions = builder.firestoreOptions; } /** @@ -193,6 +196,19 @@ ThreadManager getThreadManager() { return threadManager; } + FirestoreOptions getFirestoreOptions() { + return firestoreOptions; + } + + /** + * Creates an empty builder. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + /** * Builder for constructing {@link FirebaseOptions}. */ @@ -213,6 +229,7 @@ public static final class Builder { private String serviceAccountId; private GoogleCredentials credentials; + private FirestoreOptions firestoreOptions; private HttpTransport httpTransport = Utils.getDefaultTransport(); private JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); private ThreadManager threadManager = FirebaseThreadManagers.DEFAULT_THREAD_MANAGER; @@ -239,6 +256,7 @@ public Builder(FirebaseOptions options) { threadManager = options.threadManager; connectTimeout = options.connectTimeout; readTimeout = options.readTimeout; + firestoreOptions = options.firestoreOptions; } /** @@ -384,6 +402,22 @@ public Builder setThreadManager(ThreadManager threadManager) { return this; } + /** + * Sets the FirestoreOptions used to initialize Firestore in the + * {@link com.google.firebase.cloud.FirestoreClient} API. This can be used to customize + * low-level transport (GRPC) parameters, and timestamp handling behavior. + * + *

If credentials or a project ID is set in FirestoreOptions, they will get + * overwritten by the corresponding parameters in FirebaseOptions. + * + * @param firestoreOptions A FirestoreOptions instance. + * @return This Builder instance is returned so subsequent calls can be chained. + */ + public Builder setFirestoreOptions(FirestoreOptions firestoreOptions) { + this.firestoreOptions = firestoreOptions; + return this; + } + /** * Sets the connect timeout for outgoing HTTP (REST) connections made by the SDK. This is used * when opening a communication link to a remote HTTP endpoint. This setting does not diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java index 5c173ae28..4b56e838c 100644 --- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java +++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java @@ -18,6 +18,7 @@ import com.google.api.core.ApiFuture; import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.firestore.FirestoreOptions; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; @@ -43,6 +44,10 @@ public static String getProjectId(@NonNull FirebaseApp app) { return app.getProjectId(); } + public static FirestoreOptions getFirestoreOptions(@NonNull FirebaseApp app) { + return app.getOptions().getFirestoreOptions(); + } + public static boolean isDefaultApp(@NonNull FirebaseApp app) { return app.isDefaultApp(); } diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index 210124972..15171d14b 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -3,6 +3,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.gax.core.FixedCredentialsProvider; import com.google.cloud.firestore.Firestore; import com.google.cloud.firestore.FirestoreOptions; import com.google.common.base.Strings; @@ -34,8 +35,13 @@ private FirestoreClient(FirebaseApp app) { "Project ID is required for accessing Firestore. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); - this.firestore = FirestoreOptions.newBuilder() - .setCredentials(ImplFirebaseTrampolines.getCredentials(app)) + FirestoreOptions userOptions = ImplFirebaseTrampolines.getFirestoreOptions(app); + FirestoreOptions.Builder builder = userOptions != null + ? userOptions.toBuilder() : FirestoreOptions.newBuilder(); + this.firestore = builder + // CredentialsProvider has highest priority in FirestoreOptions, so we set that. + .setCredentialsProvider( + FixedCredentialsProvider.create(ImplFirebaseTrampolines.getCredentials(app))) .setProjectId(projectId) .build() .getService(); diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index 72a9c4784..0623a3810 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -30,6 +30,7 @@ import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.firestore.FirestoreOptions; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.io.IOException; @@ -75,6 +76,9 @@ protected ThreadFactory getThreadFactory() { public void createOptionsWithAllValuesSet() throws IOException { GsonFactory jsonFactory = new GsonFactory(); NetHttpTransport httpTransport = new NetHttpTransport(); + FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .build(); FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() .setDatabaseUrl(FIREBASE_DB_URL) @@ -86,6 +90,7 @@ public void createOptionsWithAllValuesSet() throws IOException { .setThreadManager(MOCK_THREAD_MANAGER) .setConnectTimeout(30000) .setReadTimeout(60000) + .setFirestoreOptions(firestoreOptions) .build(); assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl()); assertEquals(FIREBASE_STORAGE_BUCKET, firebaseOptions.getStorageBucket()); @@ -95,6 +100,7 @@ public void createOptionsWithAllValuesSet() throws IOException { assertSame(MOCK_THREAD_MANAGER, firebaseOptions.getThreadManager()); assertEquals(30000, firebaseOptions.getConnectTimeout()); assertEquals(60000, firebaseOptions.getReadTimeout()); + assertSame(firestoreOptions, firebaseOptions.getFirestoreOptions()); GoogleCredentials credentials = firebaseOptions.getCredentials(); assertNotNull(credentials); @@ -124,6 +130,7 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { assertEquals( GoogleCredential.fromStream(ServiceAccount.EDITOR.asStream()).getServiceAccountId(), ((ServiceAccountCredentials) credentials).getClientEmail()); + assertNull(firebaseOptions.getFirestoreOptions()); } @Test @@ -185,6 +192,8 @@ public void checkToBuilderCreatesNewEquivalentInstance() { assertEquals(ALL_VALUES_OPTIONS.getThreadManager(), allValuesOptionsCopy.getThreadManager()); assertEquals(ALL_VALUES_OPTIONS.getConnectTimeout(), allValuesOptionsCopy.getConnectTimeout()); assertEquals(ALL_VALUES_OPTIONS.getReadTimeout(), allValuesOptionsCopy.getReadTimeout()); + assertSame(ALL_VALUES_OPTIONS.getFirestoreOptions(), + allValuesOptionsCopy.getFirestoreOptions()); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 1a4ccd4af..db8a03047 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -36,6 +36,7 @@ import com.google.auth.oauth2.UserCredentials; import com.google.common.base.Defaults; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -132,7 +133,8 @@ public HttpTransport create() { } private static GoogleCredentials createCertificateCredential() throws IOException { - final MockTokenServerTransport transport = new MockTokenServerTransport(); + final MockTokenServerTransport transport = new MockTokenServerTransport( + "https://accounts.google.com/o/oauth2/token"); transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); return ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream(), new HttpTransportFactory() { diff --git a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java index 737eab1fe..f7c88a5cf 100644 --- a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java +++ b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java @@ -2,12 +2,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.firestore.Firestore; +import com.google.cloud.firestore.FirestoreOptions; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.FirebaseOptions.Builder; +import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; @@ -26,6 +31,9 @@ public void testExplicitProjectId() throws IOException { FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") + .setFirestoreOptions(FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .build()) .build()); Firestore firestore = FirestoreClient.getFirestore(app); assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); @@ -38,6 +46,9 @@ public void testExplicitProjectId() throws IOException { public void testServiceAccountProjectId() throws IOException { FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) + .setFirestoreOptions(FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .build()) .build()); Firestore firestore = FirestoreClient.getFirestore(app); assertEquals("mock-project-id", firestore.getOptions().getProjectId()); @@ -46,11 +57,56 @@ public void testServiceAccountProjectId() throws IOException { assertEquals("mock-project-id", firestore.getOptions().getProjectId()); } + @Test + public void testFirestoreOptions() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(new Builder() + .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) + .setProjectId("explicit-project-id") + .setFirestoreOptions(FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .build()) + .build()); + Firestore firestore = FirestoreClient.getFirestore(app); + assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + assertTrue(firestore.getOptions().areTimestampsInSnapshotsEnabled()); + + firestore = FirestoreClient.getFirestore(); + assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + assertTrue(firestore.getOptions().areTimestampsInSnapshotsEnabled()); + } + + @Test + public void testFirestoreOptionsOverride() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(new Builder() + .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) + .setProjectId("explicit-project-id") + .setFirestoreOptions(FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .setProjectId("other-project-id") + .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) + .build()) + .build()); + Firestore firestore = FirestoreClient.getFirestore(app); + assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + assertTrue(firestore.getOptions().areTimestampsInSnapshotsEnabled()); + assertSame(ImplFirebaseTrampolines.getCredentials(app), + firestore.getOptions().getCredentialsProvider().getCredentials()); + + firestore = FirestoreClient.getFirestore(); + assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + assertTrue(firestore.getOptions().areTimestampsInSnapshotsEnabled()); + assertSame(ImplFirebaseTrampolines.getCredentials(app), + firestore.getOptions().getCredentialsProvider().getCredentials()); + } + @Test public void testAppDelete() throws IOException { FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("mock-project-id") + .setFirestoreOptions(FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .build()) .build()); assertNotNull(FirestoreClient.getFirestore(app)); diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index 00466821f..7d2b9d4ea 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -20,10 +20,12 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.GenericJson; +import com.google.cloud.firestore.FirestoreOptions; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; @@ -105,10 +107,13 @@ public static synchronized String getApiKey() { public static synchronized FirebaseApp ensureDefaultApp() { if (masterApp == null) { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(getDatabaseUrl()) .setStorageBucket(getStorageBucket()) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) + .setFirestoreOptions(FirestoreOptions.newBuilder() + .setTimestampsInSnapshotsEnabled(true) + .build()) .build(); masterApp = FirebaseApp.initializeApp(options); } diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index b889bc263..0dec4db3f 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -106,7 +106,8 @@ public static synchronized GoogleCredentials getApplicationDefaultCredentials() if (defaultCredentials != null) { return defaultCredentials; } - final MockTokenServerTransport transport = new MockTokenServerTransport(); + final MockTokenServerTransport transport = new MockTokenServerTransport( + "https://accounts.google.com/o/oauth2/token"); transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), TEST_ADC_ACCESS_TOKEN); File serviceAccount = new File("src/test/resources/service_accounts", "editor.json"); Map environmentVariables = From e938204d6880f1e1c8780889505d84b3851b746c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 14 Sep 2018 10:40:27 -0700 Subject: [PATCH 013/441] Staging the release 6.5.0 (#207) * Updating CHANGELOG for 6.5.0 release. * [maven-release-plugin] prepare release v6.5.0 * [maven-release-plugin] prepare for next development iteration * Updated changelog --- CHANGELOG.md | 7 +++++++ pom.xml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27071e330..a26bb3258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ - +# v6.5.0 + +- [added] `FirebaseOptions.Builder` class now provides a + `setFirestoreOptions()` method for configuring the Firestore client. +- [changed] Upgraded the Cloud Firestore client to 0.61.0-beta. +- [changed] Upgraded the Cloud Storage client to 1.43.0. + # v6.4.0 - [added] `WebpushNotification` type now supports arbitrary key-value diff --git a/pom.xml b/pom.xml index b82e5fc95..acc0b426f 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.4.1-SNAPSHOT + 6.5.1-SNAPSHOT jar firebase-admin From 2e44edaa45e3c173de3879e494c6c1f182094e4c Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Thu, 15 Nov 2018 19:36:50 +0100 Subject: [PATCH 014/441] fix FcmErrorCode error type (#216) --- CHANGELOG.md | 3 ++- .../java/com/google/firebase/messaging/FirebaseMessaging.java | 2 +- .../com/google/firebase/messaging/FirebaseMessagingTest.java | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a26bb3258..54c45f6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [fixed] Fixing error handling in FCM. The SDK now checks the key + type.googleapis.com/google.firebase.fcm.v1.FcmError to set error code. # v6.5.0 diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 7ba1edd4d..efdd39da0 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -56,7 +56,7 @@ public class FirebaseMessaging { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; private static final String FCM_ERROR_TYPE = - "type.googleapis.com/google.firebase.fcm.v1.FcmErrorCode"; + "type.googleapis.com/google.firebase.fcm.v1.FcmError"; private static final String INTERNAL_ERROR = "internal-error"; private static final String UNKNOWN_ERROR = "unknown-error"; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index b269978f2..0b8ce1caa 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -278,14 +278,14 @@ public void testSendErrorWithCanonicalCode() throws Exception { } @Test - public void testSendErrorWithFcmErrorCode() throws Exception { + public void testSendErrorWithFcmError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); FirebaseMessaging messaging = initMessaging(response); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmErrorCode\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); messaging.setInterceptor(interceptor); try { From d314964edfafcc2f0dd64799905ba284b4d07c3c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 21 Nov 2018 10:31:22 -0800 Subject: [PATCH 015/441] Getting more detailed errors from FCM back-end service (#217) --- CHANGELOG.md | 5 ++++- .../com/google/firebase/messaging/FirebaseMessaging.java | 1 + .../com/google/firebase/messaging/FirebaseMessagingTest.java | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c45f6e4..b08e7c289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Unreleased - [fixed] Fixing error handling in FCM. The SDK now checks the key - type.googleapis.com/google.firebase.fcm.v1.FcmError to set error code. + `type.googleapis.com/google.firebase.fcm.v1.FcmError` to set error + code. +- [fixed] FCM errors sent by the back-end now include more details + that are helpful when debugging problems. # v6.5.0 diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index efdd39da0..34ed36497 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -249,6 +249,7 @@ protected String execute() throws FirebaseMessagingException { try { HttpRequest request = requestFactory.buildPostRequest( new GenericUrl(url), new JsonHttpContent(jsonFactory, payload.build())); + request.getHeaders().set("X-GOOG-API-FORMAT-VERSION", "2"); request.setParser(new JsonObjectParser(jsonFactory)); request.setResponseInterceptor(interceptor); response = request.execute(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 0b8ce1caa..4b51513be 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -512,6 +512,7 @@ private static HttpRequest checkRequestHeader(TestResponseInterceptor intercepto assertEquals("POST", request.getRequestMethod()); assertEquals(TEST_FCM_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals("2", request.getHeaders().get("X-GOOG-API-FORMAT-VERSION")); return request; } From 6c907f6e3aedea3b6bbf1e0e53d75a621984223d Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 21 Nov 2018 15:05:37 -0800 Subject: [PATCH 016/441] Disable generation of additional Maven reports (#218) * Disable generation of additional Maven reports * Added comment --- pom.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pom.xml b/pom.xml index acc0b426f..9269f3b5a 100644 --- a/pom.xml +++ b/pom.xml @@ -370,6 +370,23 @@ + + + + maven-project-info-reports-plugin + 2.9 + + + + + true + + + + + + + From 4f21d1ee4530fe1bf542aa904ce08863d12a548e Mon Sep 17 00:00:00 2001 From: Jacob Wang <4806046+xiangkangjw@users.noreply.github.com> Date: Mon, 26 Nov 2018 16:49:42 -0500 Subject: [PATCH 017/441] Add App-scoped API For Firebase Project management (#211) App-scoped actions for Firebase apps within Firebase projects. Uses Firebase Management REST APIs (https://firebase.google.com/docs/projects/api/reference/rest/). --- .../java/com/google/firebase/FirebaseApp.java | 19 +- .../firebase/ImplFirebaseTrampolines.java | 22 + .../projectmanagement/AndroidApp.java | 166 ++++ .../projectmanagement/AndroidAppMetadata.java | 111 +++ .../projectmanagement/AndroidAppService.java | 175 ++++ .../FirebaseProjectManagement.java | 295 ++++++ .../FirebaseProjectManagementException.java | 32 + .../FirebaseProjectManagementServiceImpl.java | 761 +++++++++++++++ .../projectmanagement/HttpHelper.java | 186 ++++ .../firebase/projectmanagement/IosApp.java | 103 ++ .../projectmanagement/IosAppMetadata.java | 109 +++ .../projectmanagement/IosAppService.java | 115 +++ .../projectmanagement/ShaCertificate.java | 123 +++ .../projectmanagement/ShaCertificateType.java | 26 + .../projectmanagement/AndroidAppTest.java | 197 ++++ .../FirebaseProjectManagementIT.java | 252 +++++ ...ebaseProjectManagementServiceImplTest.java | 908 ++++++++++++++++++ .../FirebaseProjectManagementTest.java | 425 ++++++++ .../projectmanagement/IosAppTest.java | 213 ++++ .../projectmanagement/ShaCertificateTest.java | 61 ++ 20 files changed, 4297 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/google/firebase/projectmanagement/AndroidApp.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/AndroidAppService.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/HttpHelper.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/IosApp.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/IosAppService.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java create mode 100644 src/main/java/com/google/firebase/projectmanagement/ShaCertificateType.java create mode 100644 src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java create mode 100644 src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java create mode 100644 src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java create mode 100644 src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java create mode 100644 src/test/java/com/google/firebase/projectmanagement/IosAppTest.java create mode 100644 src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index 59a79e465..c7f24476b 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -276,8 +276,8 @@ public String getName() { return name; } - /** - * Returns the specified {@link FirebaseOptions}. + /** + * Returns the specified {@link FirebaseOptions}. */ @NonNull public FirebaseOptions getOptions() { @@ -390,6 +390,10 @@ ThreadFactory getThreadFactory() { return threadManager.getThreadFactory(); } + ScheduledExecutorService getScheduledExecutorService() { + return ensureScheduledExecutorService(); + } + ApiFuture submit(Callable command) { checkNotNull(command); return new ListenableFuture2ApiFuture<>(executors.getListeningExecutor().submit(command)); @@ -405,6 +409,17 @@ ScheduledFuture schedule(Callable command, long delayMillis) { } } + ScheduledFuture schedule(Runnable runnable, long delayMillis) { + checkNotNull(runnable); + try { + return ensureScheduledExecutorService() + .schedule(runnable, delayMillis, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // This may fail if the underlying ThreadFactory does not support long-lived threads. + throw new UnsupportedOperationException("Scheduled tasks not supported", e); + } + } + void startTokenRefresher() { synchronized (lock) { checkNotDeleted(); diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java index 4b56e838c..7a50e0b07 100644 --- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java +++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java @@ -16,13 +16,18 @@ package com.google.firebase; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.firestore.FirestoreOptions; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; /** @@ -67,6 +72,23 @@ public static ThreadFactory getThreadFactory(@NonNull FirebaseApp app) { return app.getThreadFactory(); } + public static ApiFuture transform( + ApiFuture input, + final ApiFunction function, + @NonNull FirebaseApp app) { + return ApiFutures.transform(input, function, app.getScheduledExecutorService()); + } + + public static ApiFuture transformAsync( + ApiFuture input, final ApiAsyncFunction function, @NonNull FirebaseApp app) { + return ApiFutures.transformAsync(input, function, app.getScheduledExecutorService()); + } + + public static ScheduledFuture schedule( + @NonNull FirebaseApp app, @NonNull Runnable runnable, long delayMillis) { + return app.schedule(runnable, delayMillis); + } + public static ApiFuture submitCallable( @NonNull FirebaseApp app, @NonNull Callable command) { return app.submit(command); diff --git a/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java b/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java new file mode 100644 index 000000000..d1af6db51 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java @@ -0,0 +1,166 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.api.core.ApiFuture; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import java.util.List; + +/** + * An instance of this class is a reference to an Android App within a Firebase Project; it can be + * used to query detailed information about the App, modify the display name of the App, or download + * the configuration file for the App. + * + *

Note: the methods in this class make RPCs. + */ +public class AndroidApp { + + private final AndroidAppService androidAppService; + private final String appId; + + AndroidApp(String appId, AndroidAppService androidAppService) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(appId), "app ID cannot be null or empty"); + this.appId = appId; + this.androidAppService = androidAppService; + } + + String getAppId() { + return appId; + } + + /** + * Retrieves detailed information about this Android App. + * + * @return an {@link AndroidAppMetadata} instance describing this App + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public AndroidAppMetadata getMetadata() throws Exception { + return androidAppService.getAndroidApp(appId); + } + + /** + * Asynchronously retrieves information about this Android App. + * + * @return an {@code ApiFuture} containing an {@link AndroidAppMetadata} instance describing this + * App + */ + public ApiFuture getMetadataAsync() { + return androidAppService.getAndroidAppAsync(appId); + } + + /** + * Updates the Display Name attribute of this Android App to the one given. + * + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public void setDisplayName(String newDisplayName) throws FirebaseProjectManagementException { + androidAppService.setAndroidDisplayName(appId, newDisplayName); + } + + /** + * Asynchronously updates the Display Name attribute of this Android App to the one given. + */ + public ApiFuture setDisplayNameAsync(String newDisplayName) { + return androidAppService.setAndroidDisplayNameAsync(appId, newDisplayName); + } + + /** + * Retrieves the configuration artifact associated with this Android App. + * + * @return a modified UTF-8 encoded {@code String} containing the contents of the artifact + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public String getConfig() throws FirebaseProjectManagementException { + return androidAppService.getAndroidConfig(appId); + } + + /** + * Asynchronously retrieves the configuration artifact associated with this Android App. + * + * @return an {@code ApiFuture} of a UTF-8 encoded {@code String} containing the contents of the + * artifact + */ + public ApiFuture getConfigAsync() { + return androidAppService.getAndroidConfigAsync(appId); + } + + /** + * Retrieves the entire list of SHA certificates associated with this Android app. + * + * @return a list of {@link ShaCertificate} containing resource name, SHA hash and certificate + * type + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public List getShaCertificates() throws FirebaseProjectManagementException { + return androidAppService.getShaCertificates(appId); + } + + /** + * Asynchronously retrieves the entire list of SHA certificates associated with this Android app. + * + * @return an {@code ApiFuture} of a list of {@link ShaCertificate} containing resource name, + * SHA hash and certificate type + */ + public ApiFuture> getShaCertificatesAsync() { + return androidAppService.getShaCertificatesAsync(appId); + } + + /** + * Adds a SHA certificate to this Android app. + * + * @param certificateToAdd the SHA certificate to be added to this Android app + * @return a {@link ShaCertificate} that was created for this Android app, containing resource + * name, SHA hash, and certificate type + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public ShaCertificate createShaCertificate(ShaCertificate certificateToAdd) + throws FirebaseProjectManagementException { + return androidAppService.createShaCertificate(appId, certificateToAdd); + } + + /** + * Asynchronously adds a SHA certificate to this Android app. + * + * @param certificateToAdd the SHA certificate to be added to this Android app + * @return a {@code ApiFuture} of a {@link ShaCertificate} that was created for this Android app, + * containing resource name, SHA hash, and certificate type + */ + public ApiFuture createShaCertificateAsync(ShaCertificate certificateToAdd) { + return androidAppService.createShaCertificateAsync(appId, certificateToAdd); + } + + /** + * Removes a SHA certificate from this Android app. + * + * @param certificateToRemove the SHA certificate to be removed from this Android app + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public void deleteShaCertificate(ShaCertificate certificateToRemove) + throws FirebaseProjectManagementException { + androidAppService.deleteShaCertificate(certificateToRemove.getName()); + } + + /** + * Asynchronously removes a SHA certificate from this Android app. + * + * @param certificateToRemove the SHA certificate to be removed from this Android app + */ + public ApiFuture deleteShaCertificateAsync(ShaCertificate certificateToRemove) { + return androidAppService.deleteShaCertificateAsync(certificateToRemove.getName()); + } + +} diff --git a/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java b/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java new file mode 100644 index 000000000..f60a0b035 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java @@ -0,0 +1,111 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; + +/** + * Contains detailed information about an Android App. Instances of this class are immutable. + */ +public class AndroidAppMetadata { + + private final String name; + private final String appId; + private final String displayName; + private final String projectId; + private final String packageName; + + AndroidAppMetadata( + String name, String appId, String displayName, String projectId, String packageName) { + this.name = Preconditions.checkNotNull(name, "Null name"); + this.appId = Preconditions.checkNotNull(appId, "Null appId"); + this.displayName = Preconditions.checkNotNull(displayName, "Null displayName"); + this.projectId = Preconditions.checkNotNull(projectId, "Null projectId"); + this.packageName = Preconditions.checkNotNull(packageName, "Null packageName"); + } + + /** + * Returns the fully qualified resource name of this Android App. + */ + public String getName() { + return name; + } + + /** + * Returns the globally unique, Firebase-assigned identifier of this Android App. This ID is + * unique even across Apps of different platforms, such as iOS Apps. + */ + public String getAppId() { + return appId; + } + + /** + * Returns the user-assigned display name of this Android App. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Returns the permanent, globally unique, user-assigned ID of the parent Project for this Android + * App. + */ + public String getProjectId() { + return projectId; + } + + /** + * Returns the canonical package name of this Android app as it would appear in Play store. + */ + public String getPackageName() { + return packageName; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper("AndroidAppMetadata") + .add("name", name) + .add("appId", appId) + .add("displayName", displayName) + .add("projectId", projectId) + .add("packageName", packageName) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof AndroidAppMetadata) { + AndroidAppMetadata that = (AndroidAppMetadata) o; + return Objects.equal(this.name, that.name) + && Objects.equal(this.appId, that.appId) + && Objects.equal(this.displayName, that.displayName) + && Objects.equal(this.projectId, that.projectId) + && Objects.equal(this.packageName, that.packageName); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(name, appId, displayName, projectId, packageName); + } + +} diff --git a/src/main/java/com/google/firebase/projectmanagement/AndroidAppService.java b/src/main/java/com/google/firebase/projectmanagement/AndroidAppService.java new file mode 100644 index 000000000..081b4467c --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/AndroidAppService.java @@ -0,0 +1,175 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.api.core.ApiFuture; +import java.util.List; + +/** + * An interface to interact with the Android-specific functionalities in the Firebase Project + * Management Service. + * + *

Note: Implementations of methods in this service may make RPCs. + */ +interface AndroidAppService { + /** + * Creates a new Android App in the given project. + * + * @param projectId the Project ID of the project in which to create the App + * @param packageName the package name of the Android App to be created + * @param displayName the user-defined display name for the Android App to be created + * @return an {@link AndroidApp} reference + */ + AndroidApp createAndroidApp(String projectId, String packageName, String displayName) + throws FirebaseProjectManagementException; + + /** + * Creates a new Android App in the given project. + * + * @param projectId the Project ID of the project in which to create the App + * @param packageName the package name of the Android App to be created + * @param displayName the user-defined display name for the Android App to be created + * @return an {@link AndroidApp} reference + */ + ApiFuture createAndroidAppAsync( + String projectId, String packageName, String displayName); + + /** + * Retrieve information about an existing Android App, identified by its App ID. + * + * @param appId the App ID of the Android App + * @return an {@link AndroidAppMetadata} instance describing the Android App + */ + AndroidAppMetadata getAndroidApp(String appId) throws FirebaseProjectManagementException; + + /** + * Asynchronously retrieves information about an existing Android App, identified by its App ID. + * + * @param appId the App ID of the iOS App + * @return an {@link AndroidAppMetadata} instance describing the Android App + */ + ApiFuture getAndroidAppAsync(String appId); + + /** + * Lists all the Android Apps belonging to the given project. The returned list cannot be + * modified. + * + * @param projectId the Project ID of the project + * @return a read-only list of {@link AndroidApp} references + */ + List listAndroidApps(String projectId) throws FirebaseProjectManagementException; + + /** + * Asynchronously lists all the Android Apps belonging to the given project. The returned list + * cannot be modified. + * + * @param projectId the project ID of the project + * @return an {@link ApiFuture} of a read-only list of {@link AndroidApp} references + */ + ApiFuture> listAndroidAppsAsync(String projectId); + + /** + * Updates the Display Name of the given Android App. + * + * @param appId the App ID of the Android App + * @param newDisplayName the new Display Name + */ + void setAndroidDisplayName(String appId, String newDisplayName) + throws FirebaseProjectManagementException; + + /** + * Asynchronously updates the Display Name of the given Android App. + * + * @param appId the App ID of the iOS App + * @param newDisplayName the new Display Name + */ + ApiFuture setAndroidDisplayNameAsync(String appId, String newDisplayName); + + /** + * Retrieves the configuration artifact associated with the specified Android App. + * + * @param appId the App ID of the Android App + * @return a modified UTF-8 encoded {@code String} containing the contents of the artifact + */ + String getAndroidConfig(String appId) throws FirebaseProjectManagementException; + + /** + * Asynchronously retrieves the configuration artifact associated with the specified Android App. + * + * @param appId the App ID of the Android App + * @return an {@link ApiFuture} of a modified UTF-8 encoded {@code String} containing the contents + * of the artifact + */ + ApiFuture getAndroidConfigAsync(String appId); + + /** + * Retrieves the entire list of SHA certificates associated with this Android App. + * + * @param appId the App ID of the Android App + * @return a list of {@link ShaCertificate} containing resource name, SHA hash and certificate + * type + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + List getShaCertificates(String appId) throws FirebaseProjectManagementException; + + /** + * Asynchronously retrieves the entire list of SHA certificates associated with this Android App. + * + * @param appId the App ID of the Android App + * @return an {@link ApiFuture} of a list of {@link ShaCertificate} containing resource name, + * SHA hash and certificate type + */ + ApiFuture> getShaCertificatesAsync(String appId); + + + /** + * Adds a SHA certificate to this Android App. + * + * @param appId the App ID of the Android App + * @param certificateToAdd the SHA certificate to be added to this Android App + * @return a {@link ShaCertificate} that was created for this Android App, containing resource + * name, SHA hash, and certificate type + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + ShaCertificate createShaCertificate(String appId, ShaCertificate certificateToAdd) + throws FirebaseProjectManagementException; + + /** + * Asynchronously adds a SHA certificate to this Android App. + * + * @param appId the App ID of the Android App + * @param certificateToAdd the SHA certificate to be added to this Android App + * @return a {@link ApiFuture} of a {@link ShaCertificate} that was created for this Android App, + * containing resource name, SHA hash, and certificate type + */ + ApiFuture createShaCertificateAsync( + String appId, ShaCertificate certificateToAdd); + + /** + * Removes a SHA certificate from this Android App. + * + * @param resourceName the fully qualified resource name of the SHA certificate + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + void deleteShaCertificate(String resourceName) throws FirebaseProjectManagementException; + + /** + * Asynchronously removes a SHA certificate from this Android App. + * + * @param resourceName the fully qualified resource name of the SHA certificate + */ + ApiFuture deleteShaCertificateAsync(String resourceName); +} diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java new file mode 100644 index 000000000..fe5472ef1 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java @@ -0,0 +1,295 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.List; + +/** + * This class is the entry point for all Firebase Project Management actions. + * + *

You can get an instance of FirebaseProjectManagement via {@link #getInstance(FirebaseApp)}, + * and then use it to modify or retrieve information about your Firebase project, as well as create, + * modify, or retrieve information about the Android or iOS Apps in your Firebase project. + */ +public class FirebaseProjectManagement { + private static final String SERVICE_ID = FirebaseProjectManagement.class.getName(); + + private static final Object GET_INSTANCE_LOCK = new Object(); + + private final String projectId; + private AndroidAppService androidAppService; + private IosAppService iosAppService; + + private FirebaseProjectManagement(String projectId) { + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access the Firebase Project Management service. Use a service " + + "account credential or set the project ID explicitly via FirebaseOptions. " + + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + + "environment variable."); + + this.projectId = projectId; + } + + @VisibleForTesting + void setAndroidAppService(AndroidAppService androidAppService) { + this.androidAppService = androidAppService; + } + + @VisibleForTesting + void setIosAppService(IosAppService iosAppService) { + this.iosAppService = iosAppService; + } + + /** + * Gets the {@link FirebaseProjectManagement} instance for the default {@link FirebaseApp}. + * + * @return the {@link FirebaseProjectManagement} instance for the default {@link FirebaseApp} + */ + @NonNull + public static FirebaseProjectManagement getInstance() { + return getInstance(FirebaseApp.getInstance()); + } + + /** + * Gets the {@link FirebaseProjectManagement} instance for the specified {@link FirebaseApp}. + * + * @return the {@link FirebaseProjectManagement} instance for the specified {@link FirebaseApp} + */ + @NonNull + public static FirebaseProjectManagement getInstance(FirebaseApp app) { + synchronized (GET_INSTANCE_LOCK) { + FirebaseProjectManagementService service = ImplFirebaseTrampolines.getService( + app, SERVICE_ID, FirebaseProjectManagementService.class); + if (service == null) { + service = + ImplFirebaseTrampolines.addService(app, new FirebaseProjectManagementService(app)); + } + return service.getInstance(); + } + } + + /* Android App */ + + /** + * Obtains an {@link AndroidApp} reference to an Android App in the associated Firebase project. + * + * @param appId the App ID that identifies this Android App. + * @see AndroidApp + */ + @NonNull + public AndroidApp getAndroidApp(@NonNull String appId) { + return new AndroidApp(appId, androidAppService); + } + + /** + * Lists all Android Apps in the associated Firebase project, returning a list of {@link + * AndroidApp} references to each. This returned list is read-only and cannot be modified. + * + * @throws FirebaseProjectManagementException if there was an error during the RPC + * @see AndroidApp + */ + @NonNull + public List listAndroidApps() throws FirebaseProjectManagementException { + return androidAppService.listAndroidApps(projectId); + } + + /** + * Asynchronously lists all Android Apps in the associated Firebase project, returning an {@code + * ApiFuture} of a list of {@link AndroidApp} references to each. This returned list is read-only + * and cannot be modified. + * + * @see AndroidApp + */ + @NonNull + public ApiFuture> listAndroidAppsAsync() { + return androidAppService.listAndroidAppsAsync(projectId); + } + + /** + * Creates a new Android App in the associated Firebase project and returns an {@link AndroidApp} + * reference to it. + * + * @param packageName the package name of the Android App to be created + * @throws FirebaseProjectManagementException if there was an error during the RPC + * @see AndroidApp + */ + @NonNull + public AndroidApp createAndroidApp(@NonNull String packageName) + throws FirebaseProjectManagementException { + return createAndroidApp(packageName, /* displayName= */ null); + } + + /** + * Creates a new Android App in the associated Firebase project and returns an {@link AndroidApp} + * reference to it. + * + * @param packageName the package name of the Android App to be created + * @param displayName a nickname for this Android App + * @throws FirebaseProjectManagementException if there was an error during the RPC + * @see AndroidApp + */ + @NonNull + public AndroidApp createAndroidApp(@NonNull String packageName, @Nullable String displayName) + throws FirebaseProjectManagementException { + return androidAppService.createAndroidApp(projectId, packageName, displayName); + } + + /** + * Asynchronously creates a new Android App in the associated Firebase project and returns an + * {@code ApiFuture} that will eventually contain the {@link AndroidApp} reference to it. + * + * @param packageName the package name of the Android App to be created + * @see AndroidApp + */ + @NonNull + public ApiFuture createAndroidAppAsync(@NonNull String packageName) { + return createAndroidAppAsync(packageName, /* displayName= */ null); + } + + /** + * Asynchronously creates a new Android App in the associated Firebase project and returns an + * {@code ApiFuture} that will eventually contain the {@link AndroidApp} reference to it. + * + * @param packageName the package name of the Android App to be created + * @param displayName a nickname for this Android App + * @see AndroidApp + */ + @NonNull + public ApiFuture createAndroidAppAsync( + @NonNull String packageName, @Nullable String displayName) { + return androidAppService.createAndroidAppAsync(projectId, packageName, displayName); + } + + /* iOS App */ + + /** + * Obtains an {@link IosApp} reference to an iOS App in the associated Firebase project. + * + * @param appId the App ID that identifies this iOS App. + * @see IosApp + */ + @NonNull + public IosApp getIosApp(@NonNull String appId) { + return new IosApp(appId, iosAppService); + } + + /** + * Lists all iOS Apps in the associated Firebase project, returning a list of {@link IosApp} + * references to each. This returned list is read-only and cannot be modified. + * + * @throws FirebaseProjectManagementException if there was an error during the RPC + * @see IosApp + */ + @NonNull + public List listIosApps() throws FirebaseProjectManagementException { + return iosAppService.listIosApps(projectId); + } + + /** + * Asynchronously lists all iOS Apps in the associated Firebase project, returning an {@code + * ApiFuture} of a list of {@link IosApp} references to each. This returned list is read-only and + * cannot be modified. + * + * @see IosApp + */ + @NonNull + public ApiFuture> listIosAppsAsync() { + return iosAppService.listIosAppsAsync(projectId); + } + + /** + * Creates a new iOS App in the associated Firebase project and returns an {@link IosApp} + * reference to it. + * + * @param bundleId the bundle ID of the iOS App to be created + * @throws FirebaseProjectManagementException if there was an error during the RPC + * @see IosApp + */ + @NonNull + public IosApp createIosApp(@NonNull String bundleId) throws FirebaseProjectManagementException { + return createIosApp(bundleId, /* displayName= */ null); + } + + /** + * Creates a new iOS App in the associated Firebase project and returns an {@link IosApp} + * reference to it. + * + * @param bundleId the bundle ID of the iOS App to be created + * @param displayName a nickname for this iOS App + * @throws FirebaseProjectManagementException if there was an error during the RPC + * @see IosApp + */ + @NonNull + public IosApp createIosApp(@NonNull String bundleId, @Nullable String displayName) + throws FirebaseProjectManagementException { + return iosAppService.createIosApp(projectId, bundleId, displayName); + } + + /** + * Asynchronously creates a new iOS App in the associated Firebase project and returns an {@code + * ApiFuture} that will eventually contain the {@link IosApp} reference to it. + * + * @param bundleId the bundle ID of the iOS App to be created + * @see IosApp + */ + @NonNull + public ApiFuture createIosAppAsync(@NonNull String bundleId) { + return createIosAppAsync(bundleId, /* displayName= */ null); + } + + /** + * Asynchronously creates a new iOS App in the associated Firebase project and returns an {@code + * ApiFuture} that will eventually contain the {@link IosApp} reference to it. + * + * @param bundleId the bundle ID of the iOS App to be created + * @param displayName a nickname for this iOS App + * @see IosApp + */ + @NonNull + public ApiFuture createIosAppAsync( + @NonNull String bundleId, @Nullable String displayName) { + return iosAppService.createIosAppAsync(projectId, bundleId, displayName); + } + + private static class FirebaseProjectManagementService + extends FirebaseService { + private final FirebaseProjectManagementServiceImpl serviceImpl; + + private FirebaseProjectManagementService(FirebaseApp app) { + super(SERVICE_ID, new FirebaseProjectManagement(ImplFirebaseTrampolines.getProjectId(app))); + serviceImpl = new FirebaseProjectManagementServiceImpl(app); + FirebaseProjectManagement serviceInstance = getInstance(); + serviceInstance.setAndroidAppService(serviceImpl); + serviceInstance.setIosAppService(serviceImpl); + } + + @Override + public void destroy() { + serviceImpl.destroy(); + } + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java new file mode 100644 index 000000000..fa939b0c4 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java @@ -0,0 +1,32 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.firebase.FirebaseException; +import com.google.firebase.internal.Nullable; + +/** + * An exception encountered while interacting with the Firebase Project Management Service. + */ +public class FirebaseProjectManagementException extends FirebaseException { + FirebaseProjectManagementException(String detailMessage) { + this(detailMessage, null); + } + + FirebaseProjectManagementException(String detailMessage, @Nullable Throwable cause) { + super(detailMessage, cause); + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java new file mode 100644 index 000000000..474b8b0cf --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -0,0 +1,761 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.util.Base64; +import com.google.api.client.util.Key; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.FirebaseRequestInitializer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppService { + + @VisibleForTesting static final String FIREBASE_PROJECT_MANAGEMENT_URL = + "https://firebase.googleapis.com"; + @VisibleForTesting static final int MAXIMUM_LIST_APPS_PAGE_SIZE = 100; + + private static final int MAXIMUM_POLLING_ATTEMPTS = 7; + private static final long POLL_BASE_WAIT_TIME_MILLIS = 500L; + private static final double POLL_EXPONENTIAL_BACKOFF_FACTOR = 1.5; + private static final String ANDROID_APPS_RESOURCE_NAME = "androidApps"; + private static final String IOS_APPS_RESOURCE_NAME = "iosApps"; + private static final String ANDROID_NAMESPACE_PROPERTY = "package_name"; + private static final String IOS_NAMESPACE_PROPERTY = "bundle_id"; + + private final FirebaseApp app; + private final HttpHelper httpHelper; + + private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction = + new CreateAndroidAppFromAppIdFunction(); + private final CreateIosAppFromAppIdFunction createIosAppFromAppIdFunction = + new CreateIosAppFromAppIdFunction(); + + FirebaseProjectManagementServiceImpl(FirebaseApp app) { + this.app = app; + this.httpHelper = new HttpHelper( + app.getOptions().getJsonFactory(), + app.getOptions().getHttpTransport().createRequestFactory( + new FirebaseRequestInitializer(app))); + } + + @VisibleForTesting + void setInterceptor(HttpResponseInterceptor interceptor) { + httpHelper.setInterceptor(interceptor); + } + + void destroy() { + // NOTE: We don't explicitly tear down anything here. Any instance of IosApp, AndroidApp, or + // FirebaseProjectManagement that depends on this instance will no longer be able to make RPC + // calls. All polling or waiting iOS or Android App creations will be interrupted, even though + // the initial creation RPC (if made successfully) is still processed normally (asynchronously) + // by the server. + } + + /* getAndroidApp */ + + @Override + public AndroidAppMetadata getAndroidApp(String appId) throws FirebaseProjectManagementException { + return getAndroidAppOp(appId).call(); + } + + @Override + public ApiFuture getAndroidAppAsync(String appId) { + return getAndroidAppOp(appId).callAsync(app); + } + + private CallableOperation getAndroidAppOp( + final String appId) { + return new CallableOperation() { + @Override + protected AndroidAppMetadata execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/-/androidApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, appId); + AndroidAppResponse parsedResponse = new AndroidAppResponse(); + httpHelper.makeGetRequest(url, parsedResponse, appId, "App ID"); + return new AndroidAppMetadata( + parsedResponse.name, + parsedResponse.appId, + Strings.nullToEmpty(parsedResponse.displayName), + parsedResponse.projectId, + parsedResponse.packageName); + } + }; + } + + /* getIosApp */ + + @Override + public IosAppMetadata getIosApp(String appId) throws FirebaseProjectManagementException { + return getIosAppOp(appId).call(); + } + + @Override + public ApiFuture getIosAppAsync(String appId) { + return getIosAppOp(appId).callAsync(app); + } + + private CallableOperation getIosAppOp( + final String appId) { + return new CallableOperation() { + @Override + protected IosAppMetadata execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/-/iosApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, appId); + IosAppResponse parsedResponse = new IosAppResponse(); + httpHelper.makeGetRequest(url, parsedResponse, appId, "App ID"); + return new IosAppMetadata( + parsedResponse.name, + parsedResponse.appId, + Strings.nullToEmpty(parsedResponse.displayName), + parsedResponse.projectId, + parsedResponse.bundleId); + } + }; + } + + /* listAndroidApps, listIosApps */ + + @Override + public List listAndroidApps(String projectId) + throws FirebaseProjectManagementException { + return listAndroidAppsOp(projectId).call(); + } + + @Override + public ApiFuture> listAndroidAppsAsync(String projectId) { + return listAndroidAppsOp(projectId).callAsync(app); + } + + @Override + public List listIosApps(String projectId) throws FirebaseProjectManagementException { + return listIosAppsOp(projectId).call(); + } + + @Override + public ApiFuture> listIosAppsAsync(String projectId) { + return listIosAppsOp(projectId).callAsync(app); + } + + private CallableOperation, FirebaseProjectManagementException> listAndroidAppsOp( + String projectId) { + return listAppsOp(projectId, ANDROID_APPS_RESOURCE_NAME, createAndroidAppFromAppIdFunction); + } + + private CallableOperation, FirebaseProjectManagementException> listIosAppsOp( + String projectId) { + return listAppsOp(projectId, IOS_APPS_RESOURCE_NAME, createIosAppFromAppIdFunction); + } + + private CallableOperation, FirebaseProjectManagementException> listAppsOp( + final String projectId, + final String platformResourceName, + final CreateAppFromAppIdFunction createAppFromAppIdFunction) { + return new CallableOperation, FirebaseProjectManagementException>() { + @Override + protected List execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/%s/%s?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + projectId, + platformResourceName, + MAXIMUM_LIST_APPS_PAGE_SIZE); + ImmutableList.Builder builder = ImmutableList.builder(); + ListAppsResponse parsedResponse; + do { + parsedResponse = new ListAppsResponse(); + httpHelper.makeGetRequest(url, parsedResponse, projectId, "Project ID"); + if (parsedResponse.apps == null) { + break; + } + for (AppResponse app : parsedResponse.apps) { + builder.add(createAppFromAppIdFunction.apply(app.appId)); + } + url = String.format( + "%s/v1beta1/projects/%s/%s?page_token=%s&page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + projectId, + platformResourceName, + parsedResponse.nextPageToken, + MAXIMUM_LIST_APPS_PAGE_SIZE); + } while (!Strings.isNullOrEmpty(parsedResponse.nextPageToken)); + return builder.build(); + } + }; + } + + private static class ListAppsResponse { + @Key("apps") + private List apps; + + @Key("nextPageToken") + private String nextPageToken; + } + + /* createAndroidApp, createIosApp */ + + @Override + public AndroidApp createAndroidApp(String projectId, String packageName, String displayName) + throws FirebaseProjectManagementException { + String operationName = createAndroidAppOp(projectId, packageName, displayName).call(); + String appId = pollOperation(projectId, operationName); + return new AndroidApp(appId, this); + } + + @Override + public ApiFuture createAndroidAppAsync( + String projectId, String packageName, String displayName) { + checkArgument(!Strings.isNullOrEmpty(packageName), "package name must not be null or empty"); + return + ImplFirebaseTrampolines.transform( + ImplFirebaseTrampolines.transformAsync( + createAndroidAppOp(projectId, packageName, displayName).callAsync(app), + new WaitOperationFunction(projectId), + app), + createAndroidAppFromAppIdFunction, + app); + } + + @Override + public IosApp createIosApp(String projectId, String bundleId, String displayName) + throws FirebaseProjectManagementException { + String operationName = createIosAppOp(projectId, bundleId, displayName).call(); + String appId = pollOperation(projectId, operationName); + return new IosApp(appId, this); + } + + @Override + public ApiFuture createIosAppAsync( + String projectId, String bundleId, String displayName) { + checkArgument(!Strings.isNullOrEmpty(bundleId), "bundle ID must not be null or empty"); + return + ImplFirebaseTrampolines.transform( + ImplFirebaseTrampolines.transformAsync( + createIosAppOp(projectId, bundleId, displayName).callAsync(app), + new WaitOperationFunction(projectId), + app), + createIosAppFromAppIdFunction, + app); + } + + private CallableOperation createAndroidAppOp( + String projectId, String namespace, String displayName) { + return createAppOp( + projectId, namespace, displayName, ANDROID_NAMESPACE_PROPERTY, ANDROID_APPS_RESOURCE_NAME); + } + + private CallableOperation createIosAppOp( + String projectId, String namespace, String displayName) { + return createAppOp( + projectId, namespace, displayName, IOS_NAMESPACE_PROPERTY, IOS_APPS_RESOURCE_NAME); + } + + private CallableOperation createAppOp( + final String projectId, + final String namespace, + final String displayName, + final String platformNamespaceProperty, + final String platformResourceName) { + return new CallableOperation() { + @Override + protected String execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/%s/%s", + FIREBASE_PROJECT_MANAGEMENT_URL, + projectId, + platformResourceName); + ImmutableMap.Builder payloadBuilder = + ImmutableMap.builder().put(platformNamespaceProperty, namespace); + if (!Strings.isNullOrEmpty(displayName)) { + payloadBuilder.put("display_name", displayName); + } + OperationResponse operationResponseInstance = new OperationResponse(); + httpHelper.makePostRequest( + url, payloadBuilder.build(), operationResponseInstance, projectId, "Project ID"); + if (Strings.isNullOrEmpty(operationResponseInstance.name)) { + throw HttpHelper.createFirebaseProjectManagementException( + namespace, + "Bundle ID", + "Unable to create App: server returned null operation name.", + /* cause= */ null); + } + return operationResponseInstance.name; + } + }; + } + + private String pollOperation(String projectId, String operationName) + throws FirebaseProjectManagementException { + String url = String.format("%s/v1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, operationName); + for (int currentAttempt = 0; currentAttempt < MAXIMUM_POLLING_ATTEMPTS; currentAttempt++) { + long delayMillis = (long) ( + POLL_BASE_WAIT_TIME_MILLIS + * Math.pow(POLL_EXPONENTIAL_BACKOFF_FACTOR, currentAttempt)); + sleepOrThrow(projectId, delayMillis); + OperationResponse operationResponseInstance = new OperationResponse(); + httpHelper.makeGetRequest(url, operationResponseInstance, projectId, "Project ID"); + if (!operationResponseInstance.done) { + continue; + } + // The Long Running Operation API guarantees that when done == true, exactly one of 'response' + // or 'error' is set. + if (operationResponseInstance.response == null + || Strings.isNullOrEmpty(operationResponseInstance.response.appId)) { + throw HttpHelper.createFirebaseProjectManagementException( + projectId, + "Project ID", + "Unable to create App: internal server error.", + /* cause= */ null); + } + return operationResponseInstance.response.appId; + } + throw HttpHelper.createFirebaseProjectManagementException( + projectId, + "Project ID", + "Unable to create App: deadline exceeded.", + /* cause= */ null); + } + + /** + * An {@link ApiAsyncFunction} that transforms a Long Running Operation name to an {@link IosApp} + * or an {@link AndroidApp} instance by repeatedly polling the server (with exponential backoff) + * until the App is created successfully, or until the number of poll attempts exceeds the maximum + * allowed. + */ + private class WaitOperationFunction implements ApiAsyncFunction { + private final String projectId; + + private WaitOperationFunction(String projectId) { + this.projectId = projectId; + } + + /** + * Returns an {@link ApiFuture} that will eventually contain the App ID of the new created App, + * or an exception if an error occurred during polling. + */ + @Override + public ApiFuture apply(String operationName) throws FirebaseProjectManagementException { + SettableApiFuture settableFuture = SettableApiFuture.create(); + ImplFirebaseTrampolines.schedule( + app, + new WaitOperationRunnable( + /* numberOfPreviousPolls= */ 0, + operationName, + projectId, + settableFuture), + /* delayMillis= */ POLL_BASE_WAIT_TIME_MILLIS); + return settableFuture; + } + } + + /** + * A poller that repeatedly polls a Long Running Operation (with exponential backoff) until its + * status is "done", or until the number of polling attempts exceeds the maximum allowed. + */ + private class WaitOperationRunnable implements Runnable { + private final int numberOfPreviousPolls; + private final String operationName; + private final String projectId; + private final SettableApiFuture settableFuture; + + private WaitOperationRunnable( + int numberOfPreviousPolls, + String operationName, + String projectId, + SettableApiFuture settableFuture) { + this.numberOfPreviousPolls = numberOfPreviousPolls; + this.operationName = operationName; + this.projectId = projectId; + this.settableFuture = settableFuture; + } + + @Override + public void run() { + String url = String.format("%s/v1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, operationName); + OperationResponse operationResponseInstance = new OperationResponse(); + try { + httpHelper.makeGetRequest(url, operationResponseInstance, projectId, "Project ID"); + } catch (FirebaseProjectManagementException e) { + settableFuture.setException(e); + return; + } + if (!operationResponseInstance.done) { + if (numberOfPreviousPolls + 1 >= MAXIMUM_POLLING_ATTEMPTS) { + settableFuture.setException(HttpHelper.createFirebaseProjectManagementException( + projectId, + "Project ID", + "Unable to create App: deadline exceeded.", + /* cause= */ null)); + } else { + long delayMillis = (long) ( + POLL_BASE_WAIT_TIME_MILLIS + * Math.pow(POLL_EXPONENTIAL_BACKOFF_FACTOR, numberOfPreviousPolls + 1)); + ImplFirebaseTrampolines.schedule( + app, + new WaitOperationRunnable( + numberOfPreviousPolls + 1, + operationName, + projectId, + settableFuture), + delayMillis); + } + return; + } + // The Long Running Operation API guarantees that when done == true, exactly one of 'response' + // or 'error' is set. + if (operationResponseInstance.response == null + || Strings.isNullOrEmpty(operationResponseInstance.response.appId)) { + settableFuture.setException(HttpHelper.createFirebaseProjectManagementException( + projectId, + "Project ID", + "Unable to create App: internal server error.", + /* cause= */ null)); + } else { + settableFuture.set(operationResponseInstance.response.appId); + } + } + } + + // This class is public due to the way parsing nested JSON objects work, and is needed by + // create{Android|Ios}App and list{Android|Ios}Apps. In any case, the containing class, + // FirebaseProjectManagementServiceImpl, is package-private. + public static class AppResponse { + @Key("name") + protected String name; + + @Key("appId") + protected String appId; + + @Key("displayName") + protected String displayName; + + @Key("projectId") + protected String projectId; + } + + private static class AndroidAppResponse extends AppResponse { + @Key("packageName") + private String packageName; + } + + private static class IosAppResponse extends AppResponse { + @Key("bundleId") + private String bundleId; + } + + // This class is public due to the way parsing nested JSON objects work, and is needed by + // createIosApp and createAndroidApp. In any case, the containing class, + // FirebaseProjectManagementServiceImpl, is package-private. + public static class StatusResponse { + @Key("code") + private int code; + + @Key("message") + private String message; + } + + private static class OperationResponse { + @Key("name") + private String name; + + @Key("metadata") + private String metadata; + + @Key("done") + private boolean done; + + @Key("error") + private StatusResponse error; + + @Key("response") + private AppResponse response; + } + + /* setAndroidDisplayName, setIosDisplayName */ + + @Override + public void setAndroidDisplayName(String appId, String newDisplayName) + throws FirebaseProjectManagementException { + setAndroidDisplayNameOp(appId, newDisplayName).call(); + } + + @Override + public ApiFuture setAndroidDisplayNameAsync(String appId, String newDisplayName) { + return setAndroidDisplayNameOp(appId, newDisplayName).callAsync(app); + } + + @Override + public void setIosDisplayName(String appId, String newDisplayName) + throws FirebaseProjectManagementException { + setIosDisplayNameOp(appId, newDisplayName).call(); + } + + @Override + public ApiFuture setIosDisplayNameAsync(String appId, String newDisplayName) { + return setIosDisplayNameOp(appId, newDisplayName).callAsync(app); + } + + private CallableOperation setAndroidDisplayNameOp( + String appId, String newDisplayName) { + return setDisplayNameOp(appId, newDisplayName, ANDROID_APPS_RESOURCE_NAME); + } + + private CallableOperation setIosDisplayNameOp( + String appId, String newDisplayName) { + return setDisplayNameOp(appId, newDisplayName, IOS_APPS_RESOURCE_NAME); + } + + private CallableOperation setDisplayNameOp( + final String appId, final String newDisplayName, final String platformResourceName) { + checkArgument( + !Strings.isNullOrEmpty(newDisplayName), "new Display Name must not be null or empty"); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/-/%s/%s?update_mask=display_name", + FIREBASE_PROJECT_MANAGEMENT_URL, + platformResourceName, + appId); + ImmutableMap payload = + ImmutableMap.builder().put("display_name", newDisplayName).build(); + EmptyResponse emptyResponseInstance = new EmptyResponse(); + httpHelper.makePatchRequest(url, payload, emptyResponseInstance, appId, "App ID"); + return null; + } + }; + } + + private static class EmptyResponse {} + + /* getAndroidConfig, getIosConfig */ + + @Override + public String getAndroidConfig(String appId) throws FirebaseProjectManagementException { + return getAndroidConfigOp(appId).call(); + } + + @Override + public ApiFuture getAndroidConfigAsync(String appId) { + return getAndroidConfigOp(appId).callAsync(app); + } + + @Override + public String getIosConfig(String appId) throws FirebaseProjectManagementException { + return getIosConfigOp(appId).call(); + } + + @Override + public ApiFuture getIosConfigAsync(String appId) { + return getIosConfigOp(appId).callAsync(app); + } + + private CallableOperation getAndroidConfigOp( + String appId) { + return getConfigOp(appId, ANDROID_APPS_RESOURCE_NAME); + } + + private CallableOperation getIosConfigOp( + String appId) { + return getConfigOp(appId, IOS_APPS_RESOURCE_NAME); + } + + private CallableOperation getConfigOp( + final String appId, final String platformResourceName) { + return new CallableOperation() { + @Override + protected String execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/-/%s/%s/config", + FIREBASE_PROJECT_MANAGEMENT_URL, + platformResourceName, + appId); + AppConfigResponse parsedResponse = new AppConfigResponse(); + httpHelper.makeGetRequest(url, parsedResponse, appId, "App ID"); + return new String( + Base64.decodeBase64(parsedResponse.configFileContents), StandardCharsets.UTF_8); + } + }; + } + + private static class AppConfigResponse { + @Key("configFilename") + String configFilename; + + @Key("configFileContents") + String configFileContents; + } + + /* getShaCertificates */ + + @Override + public List getShaCertificates(String appId) + throws FirebaseProjectManagementException { + return getShaCertificatesOp(appId).call(); + } + + @Override + public ApiFuture> getShaCertificatesAsync(String appId) { + return getShaCertificatesOp(appId).callAsync(app); + } + + private CallableOperation, FirebaseProjectManagementException> + getShaCertificatesOp(final String appId) { + return new CallableOperation, FirebaseProjectManagementException>() { + @Override + protected List execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/-/androidApps/%s/sha", FIREBASE_PROJECT_MANAGEMENT_URL, appId); + ListShaCertificateResponse parsedResponse = new ListShaCertificateResponse(); + httpHelper.makeGetRequest(url, parsedResponse, appId, "App ID"); + List certificates = new ArrayList<>(); + if (parsedResponse.certificates == null) { + return certificates; + } + for (ShaCertificateResponse certificate : parsedResponse.certificates) { + certificates.add(ShaCertificate.create(certificate.name, certificate.shaHash)); + } + return certificates; + } + }; + } + + /* createShaCertificate */ + + @Override + public ShaCertificate createShaCertificate(String appId, ShaCertificate certificateToAdd) + throws FirebaseProjectManagementException { + return createShaCertificateOp(appId, certificateToAdd).call(); + } + + @Override + public ApiFuture createShaCertificateAsync( + String appId, ShaCertificate certificateToAdd) { + return createShaCertificateOp(appId, certificateToAdd).callAsync(app); + } + + private CallableOperation + createShaCertificateOp(final String appId, final ShaCertificate certificateToAdd) { + return new CallableOperation() { + @Override + protected ShaCertificate execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/projects/-/androidApps/%s/sha", FIREBASE_PROJECT_MANAGEMENT_URL, appId); + ShaCertificateResponse parsedResponse = new ShaCertificateResponse(); + ImmutableMap payload = ImmutableMap.builder() + .put("sha_hash", certificateToAdd.getShaHash()) + .put("cert_type", certificateToAdd.getCertType().toString()) + .build(); + httpHelper.makePostRequest(url, payload, parsedResponse, appId, "App ID"); + return ShaCertificate.create(parsedResponse.name, parsedResponse.shaHash); + } + }; + } + + /* deleteShaCertificate */ + + @Override + public void deleteShaCertificate(String resourceName) + throws FirebaseProjectManagementException { + deleteShaCertificateOp(resourceName).call(); + } + + @Override + public ApiFuture deleteShaCertificateAsync(String resourceName) { + return deleteShaCertificateOp(resourceName).callAsync(app); + } + + private CallableOperation deleteShaCertificateOp( + final String resourceName) { + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseProjectManagementException { + String url = String.format( + "%s/v1beta1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, resourceName); + EmptyResponse parsedResponse = new EmptyResponse(); + httpHelper.makeDeleteRequest(url, parsedResponse, resourceName, "SHA name"); + return null; + } + }; + } + + private static class ListShaCertificateResponse { + @Key("certificates") + private List certificates; + } + + // This class is public due to the way parsing nested JSON objects work, and is needed by + // getShaCertificates. In any case, the containing class, FirebaseProjectManagementServiceImpl, is + // package-private. + public static class ShaCertificateResponse { + @Key("name") + private String name; + + @Key("shaHash") + private String shaHash; + + @Key("certType") + private String certType; + } + + /* Helper methods. */ + + private void sleepOrThrow(String projectId, long delayMillis) + throws FirebaseProjectManagementException { + try { + Thread.sleep(delayMillis); + } catch (InterruptedException e) { + throw HttpHelper.createFirebaseProjectManagementException( + projectId, + "Project ID", + "Unable to create App: exponential backoff interrupted.", + /* cause= */ null); + } + } + + /* Helper types. */ + + private interface CreateAppFromAppIdFunction extends ApiFunction {} + + private class CreateAndroidAppFromAppIdFunction + implements CreateAppFromAppIdFunction { + @Override + public AndroidApp apply(String appId) { + return new AndroidApp(appId, FirebaseProjectManagementServiceImpl.this); + } + } + + private class CreateIosAppFromAppIdFunction implements CreateAppFromAppIdFunction { + @Override + public IosApp apply(String appId) { + return new IosApp(appId, FirebaseProjectManagementServiceImpl.this); + } + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java new file mode 100644 index 000000000..2143c1453 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java @@ -0,0 +1,186 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import java.io.IOException; + +class HttpHelper { + + @VisibleForTesting static final String PATCH_OVERRIDE_KEY = "X-HTTP-Method-Override"; + @VisibleForTesting static final String PATCH_OVERRIDE_VALUE = "PATCH"; + private static final ImmutableMap ERROR_CODES = + ImmutableMap.builder() + .put(401, "Request not authorized.") + .put(403, "Client does not have sufficient privileges.") + .put(404, "Failed to find the resource.") + .put(409, "The resource already exists.") + .put(429, "Request throttled by the backend server.") + .put(500, "Internal server error.") + .put(503, "Backend servers are over capacity. Try again later.") + .build(); + private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + + private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + private final JsonFactory jsonFactory; + private final HttpRequestFactory requestFactory; + private HttpResponseInterceptor interceptor; + + HttpHelper(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + this.jsonFactory = jsonFactory; + this.requestFactory = requestFactory; + } + + void setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + } + + void makeGetRequest( + String url, + T parsedResponseInstance, + String requestIdentifier, + String requestIdentifierDescription) throws FirebaseProjectManagementException { + try { + makeRequest( + requestFactory.buildGetRequest(new GenericUrl(url)), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); + } catch (IOException e) { + handleError(requestIdentifier, requestIdentifierDescription, e); + } + } + + void makePostRequest( + String url, + Object payload, + T parsedResponseInstance, + String requestIdentifier, + String requestIdentifierDescription) throws FirebaseProjectManagementException { + try { + makeRequest( + requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); + } catch (IOException e) { + handleError(requestIdentifier, requestIdentifierDescription, e); + } + } + + void makePatchRequest( + String url, + Object payload, + T parsedResponseInstance, + String requestIdentifier, + String requestIdentifierDescription) throws FirebaseProjectManagementException { + try { + HttpRequest baseRequest = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + baseRequest.getHeaders().set(PATCH_OVERRIDE_KEY, PATCH_OVERRIDE_VALUE); + makeRequest( + baseRequest, parsedResponseInstance, requestIdentifier, requestIdentifierDescription); + } catch (IOException e) { + handleError(requestIdentifier, requestIdentifierDescription, e); + } + } + + void makeDeleteRequest( + String url, + T parsedResponseInstance, + String requestIdentifier, + String requestIdentifierDescription) throws FirebaseProjectManagementException { + try { + makeRequest( + requestFactory.buildDeleteRequest(new GenericUrl(url)), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); + } catch (IOException e) { + handleError(requestIdentifier, requestIdentifierDescription, e); + } + } + + void makeRequest( + HttpRequest baseRequest, + T parsedResponseInstance, + String requestIdentifier, + String requestIdentifierDescription) throws FirebaseProjectManagementException { + HttpResponse response = null; + try { + baseRequest.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion); + baseRequest.setParser(new JsonObjectParser(jsonFactory)); + baseRequest.setResponseInterceptor(interceptor); + response = baseRequest.execute(); + jsonFactory.createJsonParser(response.getContent(), Charsets.UTF_8) + .parseAndClose(parsedResponseInstance); + } catch (Exception e) { + handleError(requestIdentifier, requestIdentifierDescription, e); + } finally { + disconnectQuietly(response); + } + } + + private static void disconnectQuietly(HttpResponse response) { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // Ignored. + } + } + } + + private static void handleError( + String requestIdentifier, String requestIdentifierDescription, Exception e) + throws FirebaseProjectManagementException { + String messageBody = "Error while invoking Firebase Project Management service."; + if (e instanceof HttpResponseException) { + int statusCode = ((HttpResponseException) e).getStatusCode(); + if (ERROR_CODES.containsKey(statusCode)) { + messageBody = ERROR_CODES.get(statusCode); + } + } + throw createFirebaseProjectManagementException( + requestIdentifier, requestIdentifierDescription, messageBody, e); + } + + static FirebaseProjectManagementException createFirebaseProjectManagementException( + String requestIdentifier, + String requestIdentifierDescription, + String messageBody, + @Nullable Exception cause) { + return new FirebaseProjectManagementException( + String.format( + "%s \"%s\": %s", requestIdentifierDescription, requestIdentifier, messageBody), + cause); + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/IosApp.java b/src/main/java/com/google/firebase/projectmanagement/IosApp.java new file mode 100644 index 000000000..afd54e711 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/IosApp.java @@ -0,0 +1,103 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.api.core.ApiFuture; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +/** + * An instance of this class is a reference to an iOS App within a Firebase Project; it can be used + * to query detailed information about the App, modify the display name of the App, or download the + * configuration file for the App. + * + *

Note: the methods in this class make RPCs. + */ +public class IosApp { + private final String appId; + private final IosAppService iosAppService; + + IosApp(String appId, IosAppService iosAppService) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(appId), "app ID cannot be null or empty"); + this.appId = appId; + this.iosAppService = iosAppService; + } + + String getAppId() { + return appId; + } + + /** + * Retrieves detailed information about this iOS App. + * + * @return an {@link IosAppMetadata} instance describing this App + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public IosAppMetadata getMetadata() throws FirebaseProjectManagementException { + return iosAppService.getIosApp(appId); + } + + /** + * Asynchronously retrieves information about this iOS App. + * + * @return an {@code ApiFuture} containing an {@link IosAppMetadata} instance describing this App + */ + public ApiFuture getMetadataAsync() { + return iosAppService.getIosAppAsync(appId); + } + + /** + * Updates the Display Name attribute of this iOS App to the one given. + * + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public void setDisplayName(String newDisplayName) throws FirebaseProjectManagementException { + iosAppService.setIosDisplayName(appId, newDisplayName); + } + + /** + * Asynchronously updates the Display Name attribute of this iOS App to the one given. + */ + public ApiFuture setDisplayNameAsync(String newDisplayName) { + return iosAppService.setIosDisplayNameAsync(appId, newDisplayName); + } + + /** + * Retrieves the configuration artifact associated with this iOS App. + * + * @return a modified UTF-8 encoded {@code String} containing the contents of the artifact + * @throws FirebaseProjectManagementException if there was an error during the RPC + */ + public String getConfig() throws FirebaseProjectManagementException { + return iosAppService.getIosConfig(appId); + } + + /** + * Asynchronously retrieves the configuration artifact associated with this iOS App. + * + * @return an {@code ApiFuture} of a UTF-8 encoded {@code String} containing the contents of the + * artifact + */ + public ApiFuture getConfigAsync() { + return iosAppService.getIosConfigAsync(appId); + } + + @Override + public String toString() { + return String.format("iOS App %s", getAppId()); + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java b/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java new file mode 100644 index 000000000..bbd8167b4 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java @@ -0,0 +1,109 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.api.client.util.Preconditions; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +/** + * Contains detailed information about an iOS App. Instances of this class are immutable. + */ +public class IosAppMetadata { + private final String name; + private final String appId; + private final String displayName; + private final String projectId; + private final String bundleId; + + IosAppMetadata( + String name, String appId, String displayName, String projectId, String bundleId) { + this.name = Preconditions.checkNotNull(name, "Null name"); + this.appId = Preconditions.checkNotNull(appId, "Null appId"); + this.displayName = Preconditions.checkNotNull(displayName, "Null displayName"); + this.projectId = Preconditions.checkNotNull(projectId, "Null projectId"); + this.bundleId = Preconditions.checkNotNull(bundleId, "Null bundleId"); + } + + /** + * Returns the fully qualified resource name of this iOS App. + */ + public String getName() { + return name; + } + + /** + * Returns the globally unique, Firebase-assigned identifier of this iOS App. This ID is unique + * even across Apps of different platforms, such as Android Apps. + */ + public String getAppId() { + return appId; + } + + /** + * Returns the user-assigned display name of this iOS App. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Returns the permanent, globally unique, user-assigned ID of the parent Project for this iOS + * App. + */ + public String getProjectId() { + return projectId; + } + + /** + * Returns the canonical bundle ID of this iOS App as it would appear in the iOS AppStore. + */ + public String getBundleId() { + return bundleId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper("IosAppMetadata") + .add("name", name) + .add("appId", appId) + .add("displayName", displayName) + .add("projectId", projectId) + .add("bundleId", bundleId) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof IosAppMetadata) { + IosAppMetadata that = (IosAppMetadata) o; + return Objects.equal(this.name, that.name) + && Objects.equal(this.appId, that.appId) + && Objects.equal(this.displayName, that.displayName) + && Objects.equal(this.projectId, that.projectId) + && Objects.equal(this.bundleId, that.bundleId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(name, appId, displayName, projectId, bundleId); + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/IosAppService.java b/src/main/java/com/google/firebase/projectmanagement/IosAppService.java new file mode 100644 index 000000000..c1e68faf8 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/IosAppService.java @@ -0,0 +1,115 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.api.core.ApiFuture; +import java.util.List; + +/** + * An interface to interact with the iOS-specific functionalities in the Firebase Project Management + * Service. + * + *

Note: Implementations of methods in this service may make RPCs. + */ +interface IosAppService { + /** + * Creates a new iOS App in the given project with the given display name. + * + * @param projectId the Project ID of the project in which to create the App + * @param bundleId the bundle ID of the iOS App to create + * @param displayName a nickname for this iOS App + * @return an {@link IosApp} reference + */ + IosApp createIosApp(String projectId, String bundleId, String displayName) + throws FirebaseProjectManagementException; + + /** + * Asynchronously creates a new iOS App in the given project with the given display name. + * + * @param projectId the Project ID of the project in which to create the App + * @param bundleId the bundle ID of the iOS App to create + * @param displayName a nickname for this iOS App + * @return an {@link ApiFuture} of an {@link IosApp} reference + */ + ApiFuture createIosAppAsync(String projectId, String bundleId, String displayName); + + /** + * Retrieve information about an existing iOS App, identified by its App ID. + * + * @param appId the App ID of the iOS App + * @return an {@link IosAppMetadata} instance describing the iOS App + */ + IosAppMetadata getIosApp(String appId) throws FirebaseProjectManagementException; + + /** + * Asynchronously retrieves information about an existing iOS App, identified by its App ID. + * + * @param appId the App ID of the iOS App + * @return an {@link IosAppMetadata} instance describing the iOS App + */ + ApiFuture getIosAppAsync(String appId); + + /** + * Lists all the iOS Apps belonging to the given project. The returned list cannot be modified. + * + * @param projectId the Project ID of the project + * @return a read-only list of {@link IosApp} references + */ + List listIosApps(String projectId) throws FirebaseProjectManagementException; + + /** + * Asynchronously lists all the iOS Apps belonging to the given project. The returned list cannot + * be modified. + * + * @param projectId the project ID of the project + * @return an {@link ApiFuture} of a read-only list of {@link IosApp} references + */ + ApiFuture> listIosAppsAsync(String projectId); + + /** + * Updates the Display Name of the given iOS App. + * + * @param appId the App ID of the iOS App + * @param newDisplayName the new Display Name + */ + void setIosDisplayName(String appId, String newDisplayName) + throws FirebaseProjectManagementException; + + /** + * Asynchronously updates the Display Name of the given iOS App. + * + * @param appId the App ID of the iOS App + * @param newDisplayName the new Display Name + */ + ApiFuture setIosDisplayNameAsync(String appId, String newDisplayName); + + /** + * Retrieves the configuration artifact associated with the specified iOS App. + * + * @param appId the App ID of the iOS App + * @return a modified UTF-8 encoded {@code String} containing the contents of the artifact + */ + String getIosConfig(String appId) throws FirebaseProjectManagementException; + + /** + * Asynchronously retrieves the configuration artifact associated with the specified iOS App. + * + * @param appId the App ID of the iOS App + * @return an {@link ApiFuture} of a modified UTF-8 encoded {@code String} containing the contents + * of the artifact + */ + ApiFuture getIosConfigAsync(String appId); +} diff --git a/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java b/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java new file mode 100644 index 000000000..805ac3dad --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java @@ -0,0 +1,123 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ascii; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import java.util.regex.Pattern; + +/** + * Contains detailed information of a SHA certificate, which can be associated to an Android app. + */ +public class ShaCertificate { + + private static final Pattern SHA1_PATTERN = Pattern.compile("[0-9a-fA-F]{40}"); + private static final Pattern SHA256_PATTERN = Pattern.compile("[0-9a-fA-F]{64}"); + + private final String name; + private final String shaHash; + private final ShaCertificateType certType; + + private ShaCertificate(String name, String shaHash, ShaCertificateType certType) { + this.name = Preconditions.checkNotNull(name, "Null name"); + this.shaHash = Preconditions.checkNotNull(shaHash, "Null shaHash"); + this.certType = Preconditions.checkNotNull(certType, "Null certType"); + } + + /** + * Creates a {@link ShaCertificate} from certificate hash. Name will be left as empty string since + * the certificate doesn't have a generated name yet. + * + * @param shaHash SHA hash of the certificate + * @return a SHA certificate + */ + public static ShaCertificate create(String shaHash) { + return new ShaCertificate("", shaHash, getTypeFromHash(shaHash)); + } + + static ShaCertificate create(String name, String shaHash) { + return new ShaCertificate(name, shaHash, getTypeFromHash(shaHash)); + } + + /** + * Returns the type of the certificate based on its hash. + * + * @throws IllegalArgumentException if the SHA hash is neither SHA-1 nor SHA-256 + */ + @VisibleForTesting + static ShaCertificateType getTypeFromHash(String shaHash) { + Preconditions.checkNotNull(shaHash, "Null shaHash"); + shaHash = Ascii.toLowerCase(shaHash); + if (SHA1_PATTERN.matcher(shaHash).matches()) { + return ShaCertificateType.SHA_1; + } else if (SHA256_PATTERN.matcher(shaHash).matches()) { + return ShaCertificateType.SHA_256; + } + throw new IllegalArgumentException("Invalid SHA hash, it is neither SHA-1 nor SHA-256."); + } + + /** + * Returns the fully qualified resource name of this SHA certificate. + */ + public String getName() { + return name; + } + + /** + * Returns the hash of this SHA certificate. + */ + public String getShaHash() { + return shaHash; + } + + /** + * Returns the type {@link ShaCertificateType} of this SHA certificate. + */ + public ShaCertificateType getCertType() { + return certType; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof ShaCertificate) { + ShaCertificate that = (ShaCertificate) o; + return (this.name.equals(that.getName())) + && (this.shaHash.equals(that.getShaHash())) + && (this.certType.equals(that.getCertType())); + } + return false; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper("ShaCertificate") + .add("name", name) + .add("shaHash", shaHash) + .add("certType", certType) + .toString(); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, shaHash, certType); + } +} diff --git a/src/main/java/com/google/firebase/projectmanagement/ShaCertificateType.java b/src/main/java/com/google/firebase/projectmanagement/ShaCertificateType.java new file mode 100644 index 000000000..a367ee410 --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/ShaCertificateType.java @@ -0,0 +1,26 @@ +/* Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +/** + * Enum denoting types of SHA certificates currently supported by Firebase. + */ +public enum ShaCertificateType { + /** Certificate generated by SHA-1 hashing algorithm. */ + SHA_1, + /** Certificate generated by SHA-256 hashing algorithm. */ + SHA_256 +} diff --git a/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java b/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java new file mode 100644 index 000000000..3f6529c7e --- /dev/null +++ b/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import com.google.api.core.SettableApiFuture; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class AndroidAppTest { + + private static final String APP_NAME = "mock-name"; + private static final String APP_ID = "1:12345:android:deadbeef"; + private static final String APP_DISPLAY_NAME = "test-android-app"; + private static final String NEW_DISPLAY_NAME = "new-test-android-app"; + private static final String APP_PACKAGE_NAME = "abc.def"; + private static final String PROJECT_ID = "test-project-id"; + private static final String ANDROID_CONFIG = "android.config"; + private static final String CERTIFICATE_NAME = + "projects/test-project/androidApps/1:11111:android:11111/sha/111111"; + private static final String SHA_HASH = "1111111111111111111111111111111111111111"; + private static final AndroidAppMetadata ANDROID_APP_METADATA = + new AndroidAppMetadata(APP_NAME, APP_ID, APP_DISPLAY_NAME, PROJECT_ID, APP_PACKAGE_NAME); + private static final ShaCertificate SHA_CERTIFICATE = + ShaCertificate.create(CERTIFICATE_NAME, SHA_HASH); + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock(answer = Answers.RETURNS_SMART_NULLS) + private AndroidAppService androidAppService; + + private AndroidApp androidApp; + + @Before + public void setUp() { + androidApp = new AndroidApp(APP_ID, androidAppService); + } + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetMetadata() throws Exception { + when(androidAppService.getAndroidApp(APP_ID)).thenReturn(ANDROID_APP_METADATA); + + AndroidAppMetadata metadata = androidApp.getMetadata(); + + assertEquals(metadata, ANDROID_APP_METADATA); + } + + @Test + public void testGetMetadataAsync() throws Exception { + when(androidAppService.getAndroidAppAsync(APP_ID)) + .thenReturn(createApiFuture(ANDROID_APP_METADATA)); + + AndroidAppMetadata metadata = androidApp.getMetadataAsync().get(); + + assertEquals(metadata, ANDROID_APP_METADATA); + } + + @Test + public void testSetDisplayName() throws Exception { + doNothing().when(androidAppService).setAndroidDisplayName(APP_ID, NEW_DISPLAY_NAME); + + androidApp.setDisplayName(NEW_DISPLAY_NAME); + + Mockito.verify(androidAppService).setAndroidDisplayName(APP_ID, NEW_DISPLAY_NAME); + } + + @Test + public void testSetDisplayNameAsync() throws Exception { + when(androidAppService.setAndroidDisplayNameAsync(APP_ID, NEW_DISPLAY_NAME)) + .thenReturn(createApiFuture((Void) null)); + + androidApp.setDisplayNameAsync(NEW_DISPLAY_NAME).get(); + + Mockito.verify(androidAppService).setAndroidDisplayNameAsync(APP_ID, NEW_DISPLAY_NAME); + } + + @Test + public void testGetConfig() throws Exception { + when(androidAppService.getAndroidConfig(APP_ID)).thenReturn(ANDROID_CONFIG); + + String config = androidApp.getConfig(); + + assertEquals(config, ANDROID_CONFIG); + } + + @Test + public void testGetConfigAsync() throws Exception { + when(androidAppService.getAndroidConfigAsync(APP_ID)) + .thenReturn(createApiFuture(ANDROID_CONFIG)); + + String config = androidApp.getConfigAsync().get(); + + assertEquals(config, ANDROID_CONFIG); + } + + @Test + public void testGetShaCertificates() throws Exception { + List certificateList = new ArrayList<>(); + certificateList.add(SHA_CERTIFICATE); + when(androidAppService.getShaCertificates(APP_ID)).thenReturn(certificateList); + + List res = androidApp.getShaCertificates(); + + assertEquals(res, certificateList); + } + + @Test + public void testGetShaCertificatesAsync() throws Exception { + List certificateList = new ArrayList<>(); + certificateList.add(SHA_CERTIFICATE); + when(androidAppService.getShaCertificatesAsync(APP_ID)) + .thenReturn(createApiFuture(certificateList)); + + List res = androidApp.getShaCertificatesAsync().get(); + + assertEquals(res, certificateList); + } + + @Test + public void testCreateShaCertificate() throws Exception { + when(androidAppService.createShaCertificate(APP_ID, ShaCertificate.create(SHA_HASH))) + .thenReturn(SHA_CERTIFICATE); + + ShaCertificate certificate = androidApp.createShaCertificate(ShaCertificate.create(SHA_HASH)); + + assertEquals(certificate, SHA_CERTIFICATE); + } + + @Test + public void testCreateShaCertificateAsync() throws Exception { + when(androidAppService.createShaCertificateAsync(APP_ID, ShaCertificate.create(SHA_HASH))) + .thenReturn(createApiFuture(SHA_CERTIFICATE)); + + ShaCertificate certificate = androidApp + .createShaCertificateAsync(ShaCertificate.create(SHA_HASH)).get(); + + assertEquals(certificate, SHA_CERTIFICATE); + } + + @Test + public void testDeleteShaCertificate() throws Exception { + doNothing().when(androidAppService).deleteShaCertificate(CERTIFICATE_NAME); + + androidApp.deleteShaCertificate(ShaCertificate.create(CERTIFICATE_NAME, SHA_HASH)); + + Mockito.verify(androidAppService).deleteShaCertificate(CERTIFICATE_NAME); + } + + @Test + public void testDeleteShaCertificateAsync() throws Exception { + when(androidAppService.deleteShaCertificateAsync(CERTIFICATE_NAME)) + .thenReturn(createApiFuture((Void) null)); + + androidApp.deleteShaCertificateAsync(ShaCertificate.create(CERTIFICATE_NAME, SHA_HASH)).get(); + + Mockito.verify(androidAppService).deleteShaCertificateAsync(CERTIFICATE_NAME); + } + + private SettableApiFuture createApiFuture(T value) { + final SettableApiFuture future = SettableApiFuture.create(); + future.set(value); + return future; + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java new file mode 100644 index 000000000..b6dd7a40d --- /dev/null +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java @@ -0,0 +1,252 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Strings; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.List; +import java.util.Random; +import org.junit.BeforeClass; +import org.junit.Test; + +public class FirebaseProjectManagementIT { + + private static final String TEST_APP_DISPLAY_NAME_PREFIX = + "Created By Firebase AdminSDK Java Integration Testing"; + private static final String TEST_APP_BUNDLE_ID = "com.firebase.adminsdk-java-integration-test"; + private static final String TEST_APP_PACKAGE_NAME = "com.firebase.adminsdk_java_integration_test"; + private static final String TEST_SHA1_CERTIFICATE = "1111111111111111111111111111111111111111"; + private static final String TEST_SHA256_CERTIFICATE = + "AAAACCCCAAAACCCCAAAACCCCAAAACCCCAAAACCCCAAAACCCCAAAACCCCAAAACCCC"; + private static final Random random = new Random(); + + private static String testIosAppId; + private static String testAndroidAppId; + + @BeforeClass + public static void setUpClass() throws Exception { + IntegrationTestUtils.ensureDefaultApp(); + FirebaseProjectManagement projectManagement = FirebaseProjectManagement.getInstance(); + // Ensure that we have created a Test iOS App. + List iosApps = projectManagement.listIosApps(); + for (IosApp iosApp : iosApps) { + if (iosApp.getMetadata().getDisplayName().startsWith(TEST_APP_DISPLAY_NAME_PREFIX)) { + testIosAppId = iosApp.getAppId(); + } + } + if (Strings.isNullOrEmpty(testIosAppId)) { + IosApp iosApp = + projectManagement.createIosApp(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME_PREFIX); + testIosAppId = iosApp.getAppId(); + } + // Ensure that we have created a Test Android App. + List androidApps = projectManagement.listAndroidApps(); + for (AndroidApp androidApp : androidApps) { + if (androidApp.getMetadata().getDisplayName().startsWith(TEST_APP_DISPLAY_NAME_PREFIX)) { + testAndroidAppId = androidApp.getAppId(); + } + } + if (Strings.isNullOrEmpty(testAndroidAppId)) { + AndroidApp androidApp = + projectManagement.createAndroidApp(TEST_APP_PACKAGE_NAME, TEST_APP_DISPLAY_NAME_PREFIX); + testAndroidAppId = androidApp.getAppId(); + } + } + + /* Android App Integration Tests */ + + @Test + public void testAndroidSetDisplayNameAndGetMetadata() throws Exception { + FirebaseProjectManagement projectManagement = FirebaseProjectManagement.getInstance(); + AndroidApp androidApp = projectManagement.getAndroidApp(testAndroidAppId); + + // Use the synchronous version of the API. + { + int randomInt = Math.abs(random.nextInt()); + String newDisplayName = TEST_APP_DISPLAY_NAME_PREFIX + " helloworld " + randomInt; + + androidApp.setDisplayName(newDisplayName); + AndroidAppMetadata metadata = androidApp.getMetadata(); + + assertEquals( + String.format( + "projects/%s/androidApps/%s", IntegrationTestUtils.getProjectId(), testAndroidAppId), + metadata.getName()); + assertEquals(IntegrationTestUtils.getProjectId(), metadata.getProjectId()); + assertEquals(testAndroidAppId, metadata.getAppId()); + assertEquals(newDisplayName, metadata.getDisplayName()); + assertEquals(TEST_APP_PACKAGE_NAME, metadata.getPackageName()); + } + + // Change the display name back when done. Use the asynchronous version. + { + androidApp.setDisplayNameAsync(TEST_APP_DISPLAY_NAME_PREFIX).get(); + AndroidAppMetadata metadata = androidApp.getMetadataAsync().get(); + + assertEquals( + String.format( + "projects/%s/androidApps/%s", IntegrationTestUtils.getProjectId(), testAndroidAppId), + metadata.getName()); + assertEquals(IntegrationTestUtils.getProjectId(), metadata.getProjectId()); + assertEquals(testAndroidAppId, metadata.getAppId()); + assertEquals(TEST_APP_DISPLAY_NAME_PREFIX, metadata.getDisplayName()); + assertEquals(TEST_APP_PACKAGE_NAME, metadata.getPackageName()); + } + } + + @Test + public void testAndroidCertificates() throws Exception { + FirebaseProjectManagement projectManagement = FirebaseProjectManagement.getInstance(); + AndroidApp androidApp = projectManagement.getAndroidApp(testAndroidAppId); + + // Use the Synchronous version of the API. + { + // Add SHA-1 certificate. + androidApp.createShaCertificate(ShaCertificate.create(TEST_SHA1_CERTIFICATE)); + List certificates = androidApp.getShaCertificates(); + ShaCertificate expectedCertificate = null; + for (ShaCertificate certificate : certificates) { + if (certificate.getShaHash().equals(TEST_SHA1_CERTIFICATE.toLowerCase())) { + expectedCertificate = certificate; + } + } + assertNotNull(expectedCertificate); + assertEquals(expectedCertificate.getCertType(), ShaCertificateType.SHA_1); + + // Delete SHA-1 certificate. + androidApp.deleteShaCertificate(expectedCertificate); + for (ShaCertificate certificate : androidApp.getShaCertificates()) { + if (certificate.getShaHash().equals(TEST_SHA1_CERTIFICATE)) { + fail("Test SHA-1 certificate is not deleted."); + } + } + } + + // Use the asynchronous version of the API. + { + // Add SHA-256 certificate. + androidApp.createShaCertificateAsync(ShaCertificate.create(TEST_SHA256_CERTIFICATE)).get(); + List certificates = androidApp.getShaCertificatesAsync().get(); + ShaCertificate expectedCertificate = null; + for (ShaCertificate certificate : certificates) { + if (certificate.getShaHash().equals(TEST_SHA256_CERTIFICATE.toLowerCase())) { + expectedCertificate = certificate; + } + } + assertNotNull(expectedCertificate); + assertEquals(expectedCertificate.getCertType(), ShaCertificateType.SHA_256); + + // Delete SHA-256 certificate. + androidApp.deleteShaCertificateAsync(expectedCertificate).get(); + for (ShaCertificate certificate : androidApp.getShaCertificatesAsync().get()) { + if (certificate.getShaHash().equals(TEST_SHA256_CERTIFICATE)) { + fail("Test SHA-1 certificate is not deleted."); + } + } + } + } + + @Test + public void testAndroidGetConfig() throws Exception { + FirebaseProjectManagement projectManagement = FirebaseProjectManagement.getInstance(); + AndroidApp androidApp = projectManagement.getAndroidApp(testAndroidAppId); + + // Test the synchronous version. + { + String config = androidApp.getConfig(); + + assertTrue(config.contains(IntegrationTestUtils.getProjectId())); + assertTrue(config.contains(testAndroidAppId)); + } + + // Test the asynchronous version. + { + String config = androidApp.getConfigAsync().get(); + + assertTrue(config.contains(IntegrationTestUtils.getProjectId())); + assertTrue(config.contains(testAndroidAppId)); + } + } + + /* iOS App Integration Tests */ + + @Test + public void testIosSetDisplayNameAndGetMetadata() throws Exception { + FirebaseProjectManagement projectManagement = FirebaseProjectManagement.getInstance(); + IosApp iosApp = projectManagement.getIosApp(testIosAppId); + + // Use the synchronous version of the API. + { + int randomInt = Math.abs(random.nextInt()); + String newDisplayName = TEST_APP_DISPLAY_NAME_PREFIX + " helloworld " + randomInt; + + iosApp.setDisplayName(newDisplayName); + IosAppMetadata metadata = iosApp.getMetadata(); + + assertEquals( + String.format( + "projects/%s/iosApps/%s", IntegrationTestUtils.getProjectId(), testIosAppId), + metadata.getName()); + assertEquals(IntegrationTestUtils.getProjectId(), metadata.getProjectId()); + assertEquals(testIosAppId, metadata.getAppId()); + assertEquals(newDisplayName, metadata.getDisplayName()); + assertEquals(TEST_APP_BUNDLE_ID, metadata.getBundleId()); + } + + // Change the display name back when done. Use the asynchronous version. + { + iosApp.setDisplayNameAsync(TEST_APP_DISPLAY_NAME_PREFIX).get(); + IosAppMetadata metadata = iosApp.getMetadataAsync().get(); + + assertEquals( + String.format( + "projects/%s/iosApps/%s", IntegrationTestUtils.getProjectId(), testIosAppId), + metadata.getName()); + assertEquals(IntegrationTestUtils.getProjectId(), metadata.getProjectId()); + assertEquals(testIosAppId, metadata.getAppId()); + assertEquals(TEST_APP_DISPLAY_NAME_PREFIX, metadata.getDisplayName()); + assertEquals(TEST_APP_BUNDLE_ID, metadata.getBundleId()); + } + } + + @Test + public void testIosGetConfig() throws Exception { + FirebaseProjectManagement projectManagement = FirebaseProjectManagement.getInstance(); + IosApp iosApp = projectManagement.getIosApp(testIosAppId); + + // Test the synchronous version. + { + String config = iosApp.getConfig(); + + assertTrue(config.contains(IntegrationTestUtils.getProjectId())); + assertTrue(config.contains(testIosAppId)); + } + + // Test the asynchronous version. + { + String config = iosApp.getConfigAsync().get(); + + assertTrue(config.contains(IntegrationTestUtils.getProjectId())); + assertTrue(config.contains(testIosAppId)); + } + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java new file mode 100644 index 000000000..5ba86b28e --- /dev/null +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -0,0 +1,908 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static com.google.firebase.projectmanagement.FirebaseProjectManagementServiceImpl.FIREBASE_PROJECT_MANAGEMENT_URL; +import static com.google.firebase.projectmanagement.FirebaseProjectManagementServiceImpl.MAXIMUM_LIST_APPS_PAGE_SIZE; +import static com.google.firebase.projectmanagement.HttpHelper.PATCH_OVERRIDE_KEY; +import static com.google.firebase.projectmanagement.HttpHelper.PATCH_OVERRIDE_VALUE; +import static com.google.firebase.projectmanagement.ShaCertificateType.SHA_1; +import static com.google.firebase.projectmanagement.ShaCertificateType.SHA_256; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.JsonParser; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.client.util.Base64; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +// TODO(weixifan): Add unit tests for create{Android|Ios}App. +public class FirebaseProjectManagementServiceImplTest { + + private static final String PROJECT_ID = "test-project-id"; + private static final String BUNDLE_ID = "test.ios.app"; + private static final String PACKAGE_NAME = "test.android.app"; + private static final String DISPLAY_NAME = "test-admin-sdk-app"; + private static final String ANDROID_APP_ID = "test-android-app-id"; + private static final String IOS_APP_ID = "test-ios-app-id"; + private static final String IOS_APP_RESOURCE_NAME = "ios/11111"; + private static final String ANDROID_APP_RESOURCE_NAME = "android/11111"; + private static final IosAppMetadata IOS_APP_METADATA = + new IosAppMetadata(IOS_APP_RESOURCE_NAME, IOS_APP_ID, DISPLAY_NAME, PROJECT_ID, BUNDLE_ID); + private static final AndroidAppMetadata ANDROID_APP_METADATA = + new AndroidAppMetadata( + ANDROID_APP_RESOURCE_NAME, ANDROID_APP_ID, DISPLAY_NAME, PROJECT_ID, PACKAGE_NAME); + + private static final String IOS_CONFIG_CONTENT = "ios-config-content"; + private static final String ANDROID_CONFIG_CONTENT = "android-config-content"; + private static final String SHA1_RESOURCE_NAME = "test-project/sha/11111"; + private static final String SHA1_HASH = "1111111111111111111111111111111111111111"; + private static final String SHA256_HASH = + "2222222222222222222222222222222222222222222222222222222222222222"; + + private static final String CREATE_IOS_RESPONSE = + "{\"name\" : \"operations/projects/test-project-id/apps/SomeToken\", " + + "\"done\" : \"false\"}"; + private static final String CREATE_IOS_GET_OPERATION_ATTEMPT_1_RESPONSE = + "{\"name\" : \"operations/projects/test-project-id/apps/SomeToken\", " + + "\"done\" : \"false\"}"; + private static final String CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE = + "{\"name\" : \"operations/projects/test-project-id/apps/SomeToken\", " + + "\"done\" : \"true\", " + + "\"response\" :" + + "{\"name\" : \"test-project/sha/11111\", " + + "\"appId\" : \"test-ios-app-id\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app1\"}}"; + + private static final String GET_IOS_RESPONSE = + "{\"name\" : \"ios/11111\", " + + "\"appId\" : \"test-ios-app-id\", " + + "\"displayName\" : \"%s\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app\"}"; + private static final String GET_IOS_CONFIG_RESPONSE = + "{\"configFilename\" : \"test-ios-app-config-name\", " + + "\"configFileContents\" : \"ios-config-content\"}"; + private static final String LIST_IOS_APPS_RESPONSE = + "{\"apps\": [" + + "{\"name\" : \"test-project/sha/11111\", " + + "\"appId\" : \"test-ios-app-id-1\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app1\"}, " + + "{\"name\" : \"test-project/sha/11112\", " + + "\"appId\" : \"test-ios-app-id-2\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app2\"}]}"; + + private static final String LIST_IOS_APPS_PAGE_1_RESPONSE = + "{\"apps\": [" + + "{\"name\" : \"projects/test-project-id/iosApps/test-ios-app-id-1\", " + + "\"appId\" : \"test-ios-app-id-1\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app1\"}], " + + "\"nextPageToken\" : \"next-page-token\"}"; + private static final String LIST_IOS_APPS_PAGE_2_RESPONSE = + "{\"apps\": [" + + "{\"name\" : \"projects/test-project-id/iosApps/test-ios-app-id-2\", " + + "\"appId\" : \"test-ios-app-id-2\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app2\"}]}"; + + private static final String CREATE_ANDROID_RESPONSE = + "{\"name\" : \"operations/projects/test-project-id/apps/SomeToken\", " + + "\"done\" : \"false\"}"; + private static final String CREATE_ANDROID_GET_OPERATION_ATTEMPT_1_RESPONSE = + "{\"name\" : \"operations/projects/test-project-id/apps/SomeToken\", " + + "\"done\" : \"false\"}"; + private static final String CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE = + "{\"name\" : \"operations/projects/test-project-id/apps/SomeToken\", " + + "\"done\" : \"true\", " + + "\"response\" :" + + "{\"name\" : \"test-project/sha/11111\", " + + "\"appId\" : \"test-android-app-id\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app1\"}}"; + + private static final String GET_ANDROID_RESPONSE = + "{\"name\" : \"android/11111\", " + + "\"appId\" : \"test-android-app-id\", " + + "\"displayName\" : \"%s\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app\"}"; + private static final String GET_ANDROID_CONFIG_RESPONSE = + "{\"configFilename\" : \"test-android-app-config-name\", " + + "\"configFileContents\" : \"android-config-content\"}"; + private static final String LIST_ANDROID_APPS_RESPONSE = + "{\"apps\": [" + + "{\"name\" : \"test-project/sha/11111\", " + + "\"appId\" : \"test-android-app-id-1\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app1\"}, " + + "{\"name\" : \"test-project/sha/11112\", " + + "\"appId\" : \"test-android-app-id-2\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app2\"}]}"; + + private static final String LIST_ANDROID_APPS_PAGE_1_RESPONSE = + "{\"apps\": [" + + "{\"name\" : \"projects/test-project-id/androidApps/test-android-app-id-1\", " + + "\"appId\" : \"test-android-app-id-1\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app1\"}], " + + "\"nextPageToken\" : \"next-page-token\"}"; + private static final String LIST_ANDROID_APPS_PAGE_2_RESPONSE = + "{\"apps\": [" + + "{\"name\" : \"projects/test-project-id/androidApps/test-android-app-id-2\", " + + "\"appId\" : \"test-android-app-id-2\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app2\"}]}"; + + private static final String GET_SHA_CERTIFICATES_RESPONSE = + "{\"certificates\": [" + + "{\"name\" : \"test-project/sha/11111\", " + + "\"shaHash\" : \"1111111111111111111111111111111111111111\", " + + "\"certType\" : \"SHA_1\"}, " + + "{\"name\" : \"test-project/sha/11111\", " + + "\"shaHash\" : \"2222222222222222222222222222222222222222222222222222222222222222\", " + + "\"certType\" : \"SHA_256\"}]}"; + private static final String CREATE_SHA_CERTIFICATE_RESPONSE = + "{\"name\" : \"test-project/sha/11111\", " + + "\"shaHash\" : \"%s\", " + + "\"certType\" : \"%s\"}"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private FirebaseProjectManagementServiceImpl serviceImpl; + private MockLowLevelHttpResponse firstRpcResponse; + private MultiRequestTestResponseInterceptor interceptor; + + @Before + public void setUp() { + interceptor = new MultiRequestTestResponseInterceptor(); + firstRpcResponse = new MockLowLevelHttpResponse(); + } + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void getIosApp() throws Exception { + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); + firstRpcResponse.setContent(String.format(GET_IOS_RESPONSE, DISPLAY_NAME)); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + IosAppMetadata iosAppMetadata = serviceImpl.getIosApp(IOS_APP_ID); + + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(iosAppMetadata, IOS_APP_METADATA); + } + + @Test + public void getIosAppAsync() throws Exception { + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); + firstRpcResponse.setContent(String.format(GET_IOS_RESPONSE, DISPLAY_NAME)); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + IosAppMetadata iosAppMetadata = serviceImpl.getIosAppAsync(IOS_APP_ID).get(); + + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(iosAppMetadata, IOS_APP_METADATA); + } + + @Test + public void listIosApps() throws Exception { + String expectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + firstRpcResponse.setContent(LIST_IOS_APPS_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + List iosAppList = serviceImpl.listIosApps(PROJECT_ID); + + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(iosAppList.size(), 2); + assertEquals(iosAppList.get(0).getAppId(), "test-ios-app-id-1"); + assertEquals(iosAppList.get(1).getAppId(), "test-ios-app-id-2"); + } + + @Test + public void listIosAppsAsync() throws Exception { + String expectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + firstRpcResponse.setContent(LIST_IOS_APPS_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + List iosAppList = serviceImpl.listIosAppsAsync(PROJECT_ID).get(); + + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(iosAppList.size(), 2); + assertEquals(iosAppList.get(0).getAppId(), "test-ios-app-id-1"); + assertEquals(iosAppList.get(1).getAppId(), "test-ios-app-id-2"); + } + + @Test + public void listIosAppsMultiplePages() throws Exception { + firstRpcResponse.setContent(LIST_IOS_APPS_PAGE_1_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of(firstRpcResponse, secondRpcResponse), + interceptor); + + List iosAppList = serviceImpl.listIosApps(PROJECT_ID); + + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + String secondRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_token=next-page-token&page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + assertEquals(iosAppList.size(), 2); + assertEquals(iosAppList.get(0).getAppId(), "test-ios-app-id-1"); + assertEquals(iosAppList.get(1).getAppId(), "test-ios-app-id-2"); + } + + @Test + public void listIosAppsAsyncMultiplePages() throws Exception { + firstRpcResponse.setContent(LIST_IOS_APPS_PAGE_1_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of(firstRpcResponse, secondRpcResponse), + interceptor); + + List iosAppList = serviceImpl.listIosAppsAsync(PROJECT_ID).get(); + + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + String secondRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_token=next-page-token&page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + assertEquals(iosAppList.size(), 2); + assertEquals(iosAppList.get(0).getAppId(), "test-ios-app-id-1"); + assertEquals(iosAppList.get(1).getAppId(), "test-ios-app-id-2"); + } + + @Test + public void createIosApp() throws Exception { + firstRpcResponse.setContent(CREATE_IOS_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_1_RESPONSE); + MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); + thirdRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of( + firstRpcResponse, secondRpcResponse, thirdRpcResponse), + interceptor); + + IosApp iosApp = serviceImpl.createIosApp(PROJECT_ID, BUNDLE_ID, DISPLAY_NAME); + + assertEquals(IOS_APP_ID, iosApp.getAppId()); + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps", FIREBASE_PROJECT_MANAGEMENT_URL, PROJECT_ID); + String secondRpcExpectedUrl = String.format( + "%s/v1/operations/projects/test-project-id/apps/SomeToken", + FIREBASE_PROJECT_MANAGEMENT_URL); + String thirdRpcExpectedUrl = secondRpcExpectedUrl; + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.POST); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(2, thirdRpcExpectedUrl, HttpMethod.GET); + ImmutableMap firstRpcPayload = ImmutableMap.builder() + .put("bundle_id", BUNDLE_ID) + .put("display_name", DISPLAY_NAME) + .build(); + checkRequestPayload(0, firstRpcPayload); + } + + @Test + public void createIosAppAsync() throws Exception { + firstRpcResponse.setContent(CREATE_IOS_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_1_RESPONSE); + MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); + thirdRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of( + firstRpcResponse, secondRpcResponse, thirdRpcResponse), + interceptor); + + IosApp iosApp = serviceImpl.createIosAppAsync(PROJECT_ID, BUNDLE_ID, DISPLAY_NAME).get(); + + assertEquals(IOS_APP_ID, iosApp.getAppId()); + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps", FIREBASE_PROJECT_MANAGEMENT_URL, PROJECT_ID); + String secondRpcExpectedUrl = String.format( + "%s/v1/operations/projects/test-project-id/apps/SomeToken", + FIREBASE_PROJECT_MANAGEMENT_URL); + String thirdRpcExpectedUrl = secondRpcExpectedUrl; + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.POST); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(2, thirdRpcExpectedUrl, HttpMethod.GET); + ImmutableMap firstRpcPayload = ImmutableMap.builder() + .put("bundle_id", BUNDLE_ID) + .put("display_name", DISPLAY_NAME) + .build(); + checkRequestPayload(0, firstRpcPayload); + } + + @Test + public void setIosDisplayName() throws Exception { + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + serviceImpl.setIosDisplayName(IOS_APP_ID, DISPLAY_NAME); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name", + FIREBASE_PROJECT_MANAGEMENT_URL, + IOS_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.POST); + ImmutableMap payload = + ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); + checkRequestPayload(payload); + checkPatchRequest(); + } + + @Test + public void setIosDisplayNameAsync() throws Exception { + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + serviceImpl.setIosDisplayNameAsync(IOS_APP_ID, DISPLAY_NAME).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name", + FIREBASE_PROJECT_MANAGEMENT_URL, + IOS_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.POST); + ImmutableMap payload = + ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); + checkRequestPayload(payload); + checkPatchRequest(); + } + + @Test + public void getIosConfig() throws Exception { + firstRpcResponse.setContent(GET_IOS_CONFIG_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + String content = serviceImpl.getIosConfig(IOS_APP_ID); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s/config", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(new String(Base64.decodeBase64(IOS_CONFIG_CONTENT), Charsets.UTF_8), content); + } + + @Test + public void getIosConfigAsync() throws Exception { + firstRpcResponse.setContent(GET_IOS_CONFIG_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + String content = serviceImpl.getIosConfigAsync(IOS_APP_ID).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s/config", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(new String(Base64.decodeBase64(IOS_CONFIG_CONTENT), Charsets.UTF_8), content); + } + + @Test + public void getAndroidApp() throws Exception { + firstRpcResponse.setContent(String.format(GET_ANDROID_RESPONSE, DISPLAY_NAME)); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + AndroidAppMetadata androidAppMetadata = serviceImpl.getAndroidApp(ANDROID_APP_ID); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(androidAppMetadata, ANDROID_APP_METADATA); + } + + @Test + public void getAndroidAppAsync() throws Exception { + firstRpcResponse.setContent(String.format(GET_ANDROID_RESPONSE, DISPLAY_NAME)); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + AndroidAppMetadata androidAppMetadata = serviceImpl.getAndroidAppAsync(ANDROID_APP_ID).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(androidAppMetadata, ANDROID_APP_METADATA); + } + + @Test + public void listAndroidApps() throws Exception { + firstRpcResponse.setContent(LIST_ANDROID_APPS_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + List androidAppList = serviceImpl.listAndroidApps(PROJECT_ID); + + String expectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(androidAppList.size(), 2); + assertEquals(androidAppList.get(0).getAppId(), "test-android-app-id-1"); + assertEquals(androidAppList.get(1).getAppId(), "test-android-app-id-2"); + } + + @Test + public void listAndroidAppsAsync() throws Exception { + firstRpcResponse.setContent(LIST_ANDROID_APPS_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + List androidAppList = serviceImpl.listAndroidAppsAsync(PROJECT_ID).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(androidAppList.size(), 2); + assertEquals(androidAppList.get(0).getAppId(), "test-android-app-id-1"); + assertEquals(androidAppList.get(1).getAppId(), "test-android-app-id-2"); + } + + @Test + public void listAndroidAppsMultiplePages() throws Exception { + firstRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_1_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of(firstRpcResponse, secondRpcResponse), + interceptor); + + List androidAppList = serviceImpl.listAndroidApps(PROJECT_ID); + + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + String secondRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps?page_token=next-page-token&page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + assertEquals(androidAppList.size(), 2); + assertEquals(androidAppList.get(0).getAppId(), "test-android-app-id-1"); + assertEquals(androidAppList.get(1).getAppId(), "test-android-app-id-2"); + } + + @Test + public void listAndroidAppsAsyncMultiplePages() throws Exception { + firstRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_1_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of(firstRpcResponse, secondRpcResponse), + interceptor); + + List androidAppList = serviceImpl.listAndroidAppsAsync(PROJECT_ID).get(); + + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + String secondRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps?page_token=next-page-token&page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + assertEquals(androidAppList.size(), 2); + assertEquals(androidAppList.get(0).getAppId(), "test-android-app-id-1"); + assertEquals(androidAppList.get(1).getAppId(), "test-android-app-id-2"); + } + + @Test + public void createAndroidApp() throws Exception { + firstRpcResponse.setContent(CREATE_ANDROID_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_1_RESPONSE); + MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); + thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of( + firstRpcResponse, secondRpcResponse, thirdRpcResponse), + interceptor); + + AndroidApp androidApp = + serviceImpl.createAndroidApp(PROJECT_ID, PACKAGE_NAME, DISPLAY_NAME); + + assertEquals(ANDROID_APP_ID, androidApp.getAppId()); + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps", FIREBASE_PROJECT_MANAGEMENT_URL, PROJECT_ID); + String secondRpcExpectedUrl = String.format( + "%s/v1/operations/projects/test-project-id/apps/SomeToken", + FIREBASE_PROJECT_MANAGEMENT_URL); + String thirdRpcExpectedUrl = secondRpcExpectedUrl; + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.POST); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(2, thirdRpcExpectedUrl, HttpMethod.GET); + ImmutableMap firstRpcPayload = ImmutableMap.builder() + .put("package_name", PACKAGE_NAME) + .put("display_name", DISPLAY_NAME) + .build(); + checkRequestPayload(0, firstRpcPayload); + } + + @Test + public void createAndroidAppAsync() throws Exception { + firstRpcResponse.setContent(CREATE_ANDROID_RESPONSE); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_1_RESPONSE); + MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); + thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of( + firstRpcResponse, secondRpcResponse, thirdRpcResponse), + interceptor); + + AndroidApp androidApp = + serviceImpl.createAndroidAppAsync(PROJECT_ID, PACKAGE_NAME, DISPLAY_NAME).get(); + + assertEquals(ANDROID_APP_ID, androidApp.getAppId()); + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/androidApps", FIREBASE_PROJECT_MANAGEMENT_URL, PROJECT_ID); + String secondRpcExpectedUrl = String.format( + "%s/v1/operations/projects/test-project-id/apps/SomeToken", + FIREBASE_PROJECT_MANAGEMENT_URL); + String thirdRpcExpectedUrl = secondRpcExpectedUrl; + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.POST); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(2, thirdRpcExpectedUrl, HttpMethod.GET); + ImmutableMap firstRpcPayload = ImmutableMap.builder() + .put("package_name", PACKAGE_NAME) + .put("display_name", DISPLAY_NAME) + .build(); + checkRequestPayload(0, firstRpcPayload); + } + + @Test + public void setAndroidDisplayName() throws Exception { + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + serviceImpl.setAndroidDisplayName(ANDROID_APP_ID, DISPLAY_NAME); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.POST); + ImmutableMap payload = + ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); + checkRequestPayload(payload); + checkPatchRequest(); + } + + @Test + public void setAndroidDisplayNameAsync() throws Exception { + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + serviceImpl.setAndroidDisplayNameAsync(ANDROID_APP_ID, DISPLAY_NAME).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.POST); + ImmutableMap payload = + ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); + checkRequestPayload(payload); + checkPatchRequest(); + } + + @Test + public void getAndroidConfig() throws Exception { + firstRpcResponse.setContent(GET_ANDROID_CONFIG_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + String content = serviceImpl.getAndroidConfig(ANDROID_APP_ID); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s/config", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(new String(Base64.decodeBase64(ANDROID_CONFIG_CONTENT), Charsets.UTF_8), content); + } + + @Test + public void getAndroidConfigAsync() throws Exception { + firstRpcResponse.setContent(GET_ANDROID_CONFIG_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + String content = serviceImpl.getAndroidConfigAsync(ANDROID_APP_ID).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s/config", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(new String(Base64.decodeBase64(ANDROID_CONFIG_CONTENT), Charsets.UTF_8), content); + } + + @Test + public void getShaCertificates() throws Exception { + firstRpcResponse.setContent(GET_SHA_CERTIFICATES_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + List certificateList = serviceImpl.getShaCertificates(ANDROID_APP_ID); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s/sha", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(certificateList.size(), 2); + assertEquals( + certificateList.get(0), + ShaCertificate.create("test-project/sha/11111", SHA1_HASH)); + assertEquals( + certificateList.get(1), + ShaCertificate.create("test-project/sha/11111", SHA256_HASH)); + } + + @Test + public void getShaCertificatesAsync() throws Exception { + firstRpcResponse.setContent(GET_SHA_CERTIFICATES_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + List certificateList = + serviceImpl.getShaCertificatesAsync(ANDROID_APP_ID).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s/sha", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(certificateList.size(), 2); + assertEquals( + certificateList.get(0), + ShaCertificate.create("test-project/sha/11111", SHA1_HASH)); + assertEquals( + certificateList.get(1), + ShaCertificate.create("test-project/sha/11111", SHA256_HASH)); + } + + @Test + public void createShaCertificate() throws Exception { + firstRpcResponse.setContent( + String.format(CREATE_SHA_CERTIFICATE_RESPONSE, SHA1_HASH, SHA_1.name())); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + ShaCertificate certificate = serviceImpl + .createShaCertificate(ANDROID_APP_ID, ShaCertificate.create(SHA1_HASH)); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s/sha", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.POST); + ImmutableMap payload = ImmutableMap.builder() + .put("sha_hash", SHA1_HASH) + .put("cert_type", SHA_1.toString()) + .build(); + checkRequestPayload(payload); + assertEquals(certificate, ShaCertificate.create("test-project/sha/11111", SHA1_HASH)); + } + + @Test + public void createShaCertificateAsync() throws Exception { + firstRpcResponse + .setContent(String.format(CREATE_SHA_CERTIFICATE_RESPONSE, SHA256_HASH, SHA_256.name())); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + ShaCertificate certificate = serviceImpl + .createShaCertificateAsync(ANDROID_APP_ID, ShaCertificate.create(SHA256_HASH)).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s/sha", + FIREBASE_PROJECT_MANAGEMENT_URL, + ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.POST); + ImmutableMap payload = ImmutableMap.builder() + .put("sha_hash", SHA256_HASH) + .put("cert_type", SHA_256.toString()) + .build(); + checkRequestPayload(payload); + assertEquals(ShaCertificate.create("test-project/sha/11111", SHA256_HASH), certificate); + } + + @Test + public void deleteShaCertificate() throws Exception { + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME); + + String expectedUrl = String.format( + "%s/v1beta1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, SHA1_RESOURCE_NAME); + checkRequestHeader(expectedUrl, HttpMethod.DELETE); + } + + @Test + public void deleteShaCertificateAsync() throws Exception { + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + serviceImpl.deleteShaCertificateAsync(SHA1_RESOURCE_NAME).get(); + + String expectedUrl = String.format( + "%s/v1beta1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, SHA1_RESOURCE_NAME); + checkRequestHeader(expectedUrl, HttpMethod.DELETE); + } + + private static FirebaseProjectManagementServiceImpl initServiceImpl( + MockLowLevelHttpResponse mockResponse, + MultiRequestTestResponseInterceptor interceptor) { + return initServiceImpl(ImmutableList.of(mockResponse), interceptor); + } + + private static FirebaseProjectManagementServiceImpl initServiceImpl( + List mockResponses, + MultiRequestTestResponseInterceptor interceptor) { + MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(PROJECT_ID) + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + FirebaseProjectManagementServiceImpl serviceImpl = + new FirebaseProjectManagementServiceImpl(app); + serviceImpl.setInterceptor(interceptor); + return serviceImpl; + } + + private void checkRequestHeader(String expectedUrl, HttpMethod httpMethod) { + assertEquals( + "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses()); + checkRequestHeader(0, expectedUrl, httpMethod); + } + + private void checkRequestHeader(int index, String expectedUrl, HttpMethod httpMethod) { + assertNotNull(interceptor.getResponse(index)); + HttpRequest request = interceptor.getResponse(index).getRequest(); + assertEquals(httpMethod.name(), request.getRequestMethod()); + assertEquals(expectedUrl, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } + + private void checkRequestPayload(Map expected) throws IOException { + assertEquals( + "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses()); + checkRequestPayload(0, expected); + } + + private void checkRequestPayload(int index, Map expected) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse(index).getRequest().getContent().writeTo(out); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + Map parsed = new HashMap<>(); + parser.parseAndClose(parsed); + assertEquals(expected, parsed); + } + + private void checkPatchRequest() { + assertEquals( + "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses()); + checkPatchRequest(0); + } + + private void checkPatchRequest(int index) { + assertTrue(interceptor.getResponse(index).getRequest().getHeaders() + .getHeaderStringValues(PATCH_OVERRIDE_KEY).contains(PATCH_OVERRIDE_VALUE)); + } + + private enum HttpMethod { + GET, + POST, + DELETE + } + + /** + * Can be used to intercept multiple HTTP requests and responses made by the SDK during tests. + */ + private class MultiRequestTestResponseInterceptor implements HttpResponseInterceptor { + private final List responsesList = new ArrayList<>(); + + @Override + public void interceptResponse(HttpResponse response) throws IOException { + this.responsesList.add(response); + } + + public int getNumberOfResponses() { + return responsesList.size(); + } + + public HttpResponse getResponse(int index) { + return responsesList.get(index); + } + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java new file mode 100644 index 000000000..3d51eecad --- /dev/null +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java @@ -0,0 +1,425 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static com.google.api.core.ApiFutures.immediateFuture; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class FirebaseProjectManagementTest { + + private static final String TEST_APP_ID = "1:1234567890:ios:cafef00ddeadbeef"; + private static final String TEST_APP_ID_2 = "1:1234567890:ios:f00ddeadbeefcafe"; + private static final String TEST_APP_ID_3 = "1:1234567890:ios:deadbeefcafef00d"; + private static final String TEST_APP_DISPLAY_NAME = "Hello World!"; + private static final String NULL_DISPLAY_NAME = null; + private static final String TEST_PROJECT_ID = "hello-world"; + private static final String TEST_APP_BUNDLE_ID = "com.hello.world"; + private static final FirebaseProjectManagementException FIREBASE_PROJECT_MANAGEMENT_EXCEPTION = + new FirebaseProjectManagementException("Error!", null); + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock(answer = Answers.RETURNS_SMART_NULLS) + private AndroidAppService androidAppService; + + @Mock(answer = Answers.RETURNS_SMART_NULLS) + private IosAppService iosAppService; + + private FirebaseProjectManagement projectManagement; + + @BeforeClass + public static void setUpClass() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(TEST_PROJECT_ID) + .build(); + FirebaseApp.initializeApp(options); + } + + @Before + public void setUp() { + projectManagement = FirebaseProjectManagement.getInstance(); + projectManagement.setAndroidAppService(androidAppService); + projectManagement.setIosAppService(iosAppService); + } + + @AfterClass + public static void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + /* Android */ + + @Test + public void testCreateAndroidAppShouldSucceed() throws Exception { + when(androidAppService.createAndroidApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenReturn(new AndroidApp(TEST_APP_ID, androidAppService)); + + AndroidApp androidApp = projectManagement.createAndroidApp(TEST_APP_BUNDLE_ID); + + assertEquals(TEST_APP_ID, androidApp.getAppId()); + } + + @Test + public void testCreateAndroidAppShouldRethrow() throws Exception { + when(androidAppService.createAndroidApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + try { + projectManagement.createAndroidApp(TEST_APP_BUNDLE_ID); + fail("createAndroidApp did not rethrow"); + } catch (FirebaseProjectManagementException e) { + // Pass. + } + } + + @Test + public void testCreateAndroidAppWithDisplayNameShouldSucceed() throws Exception { + when(androidAppService + .createAndroidApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenReturn(new AndroidApp(TEST_APP_ID, androidAppService)); + + AndroidApp androidApp = + projectManagement.createAndroidApp(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME); + + assertEquals(TEST_APP_ID, androidApp.getAppId()); + } + + @Test + public void testCreateAndroidAppWithDisplayNameShouldRethrow() throws Exception { + when(androidAppService + .createAndroidApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + try { + projectManagement.createAndroidApp(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME); + fail("createAndroidApp did not rethrow"); + } catch (FirebaseProjectManagementException e) { + // Pass. + } + } + + @Test + public void testCreateAndroidAppAsyncShouldSucceed() throws Exception { + when(androidAppService + .createAndroidAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenReturn(immediateFuture(new AndroidApp(TEST_APP_ID, androidAppService))); + + AndroidApp androidApp = projectManagement.createAndroidAppAsync(TEST_APP_BUNDLE_ID).get(); + + assertEquals(TEST_APP_ID, androidApp.getAppId()); + } + + @Test + public void testCreateAndroidAppAsyncShouldRethrow() throws Exception { + when(androidAppService + .createAndroidAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenReturn(immediateAndroidAppFailedFuture()); + + try { + projectManagement.createAndroidAppAsync(TEST_APP_BUNDLE_ID).get(); + fail("createAndroidAppAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + @Test + public void testCreateAndroidAppAsyncWithDisplayNameShouldSucceed() throws Exception { + when(androidAppService + .createAndroidAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenReturn(immediateFuture(new AndroidApp(TEST_APP_ID, androidAppService))); + + AndroidApp androidApp = + projectManagement.createAndroidAppAsync(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME).get(); + + assertEquals(TEST_APP_ID, androidApp.getAppId()); + } + + @Test + public void testCreateAndroidAppAsyncWithDisplayNameShouldRethrow() throws Exception { + when(androidAppService + .createAndroidAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenReturn(immediateAndroidAppFailedFuture()); + + try { + projectManagement.createAndroidAppAsync(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME).get(); + fail("createAndroidAppAsync (with display name) did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + @Test + public void testListAndroidAppsShouldSucceed() throws Exception { + AndroidApp app1 = new AndroidApp(TEST_APP_ID, androidAppService); + AndroidApp app2 = new AndroidApp(TEST_APP_ID_2, androidAppService); + AndroidApp app3 = new AndroidApp(TEST_APP_ID_3, androidAppService); + when(androidAppService.listAndroidApps(TEST_PROJECT_ID)) + .thenReturn(ImmutableList.of(app1, app2, app3)); + + List androidApps = projectManagement.listAndroidApps(); + + ImmutableSet.Builder appIdsBuilder = ImmutableSet.builder(); + for (AndroidApp androidApp : androidApps) { + appIdsBuilder.add(androidApp.getAppId()); + } + assertEquals(ImmutableSet.of(TEST_APP_ID, TEST_APP_ID_2, TEST_APP_ID_3), appIdsBuilder.build()); + } + + @Test + public void testListAndroidAppsShouldRethrow() throws Exception { + when(androidAppService.listAndroidApps(TEST_PROJECT_ID)) + .thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + try { + projectManagement.listAndroidApps(); + fail("listAndroidApps did not rethrow"); + } catch (FirebaseProjectManagementException e) { + // Pass. + } + } + + @Test + public void testListAndroidAppsAsyncShouldSucceed() throws Exception { + AndroidApp app1 = new AndroidApp(TEST_APP_ID, androidAppService); + AndroidApp app2 = new AndroidApp(TEST_APP_ID_2, androidAppService); + AndroidApp app3 = new AndroidApp(TEST_APP_ID_3, androidAppService); + List androidApps = ImmutableList.of(app1, app2, app3); + when(androidAppService.listAndroidAppsAsync(TEST_PROJECT_ID)) + .thenReturn(immediateFuture(androidApps)); + + List actualAndroidApps = projectManagement.listAndroidAppsAsync().get(); + + ImmutableSet.Builder appIdsBuilder = ImmutableSet.builder(); + for (AndroidApp androidApp : actualAndroidApps) { + appIdsBuilder.add(androidApp.getAppId()); + } + assertEquals(ImmutableSet.of(TEST_APP_ID, TEST_APP_ID_2, TEST_APP_ID_3), appIdsBuilder.build()); + } + + @Test + public void testListAndroidAppsAsyncShouldRethrow() throws Exception { + when(androidAppService.listAndroidAppsAsync(TEST_PROJECT_ID)) + .thenReturn(immediateListAndroidAppFailedFuture()); + + try { + projectManagement.listAndroidAppsAsync().get(); + fail("listAndroidAppsAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + /* iOS */ + + @Test + public void testCreateIosAppShouldSucceed() throws Exception { + when(iosAppService.createIosApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenReturn(new IosApp(TEST_APP_ID, iosAppService)); + + IosApp iosApp = projectManagement.createIosApp(TEST_APP_BUNDLE_ID); + + assertEquals(TEST_APP_ID, iosApp.getAppId()); + } + + @Test + public void testCreateIosAppShouldRethrow() throws Exception { + when(iosAppService.createIosApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + try { + projectManagement.createIosApp(TEST_APP_BUNDLE_ID); + fail("createIosApp did not rethrow"); + } catch (FirebaseProjectManagementException e) { + // Pass. + } + } + + @Test + public void testCreateIosAppWithDisplayNameShouldSucceed() throws Exception { + when(iosAppService.createIosApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenReturn(new IosApp(TEST_APP_ID, iosAppService)); + + IosApp iosApp = projectManagement.createIosApp(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME); + + assertEquals(TEST_APP_ID, iosApp.getAppId()); + } + + @Test + public void testCreateIosAppWithDisplayNameShouldRethrow() throws Exception { + when(iosAppService.createIosApp(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + try { + projectManagement.createIosApp(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME); + fail("createIosApp did not rethrow"); + } catch (FirebaseProjectManagementException e) { + // Pass. + } + } + + @Test + public void testCreateIosAppAsyncShouldSucceed() throws Exception { + when(iosAppService.createIosAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenReturn(immediateFuture(new IosApp(TEST_APP_ID, iosAppService))); + + IosApp iosApp = projectManagement.createIosAppAsync(TEST_APP_BUNDLE_ID).get(); + + assertEquals(TEST_APP_ID, iosApp.getAppId()); + } + + @Test + public void testCreateIosAppAsyncShouldRethrow() throws Exception { + when(iosAppService.createIosAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, NULL_DISPLAY_NAME)) + .thenReturn(immediateIosAppFailedFuture()); + + try { + projectManagement.createIosAppAsync(TEST_APP_BUNDLE_ID).get(); + fail("createIosAppAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + @Test + public void testCreateIosAppAsyncWithDisplayNameShouldSucceed() throws Exception { + when(iosAppService + .createIosAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenReturn(immediateFuture(new IosApp(TEST_APP_ID, iosAppService))); + + IosApp iosApp = + projectManagement.createIosAppAsync(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME).get(); + + assertEquals(TEST_APP_ID, iosApp.getAppId()); + } + + @Test + public void testCreateIosAppAsyncWithDisplayNameShouldRethrow() throws Exception { + when(iosAppService + .createIosAppAsync(TEST_PROJECT_ID, TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME)) + .thenReturn(immediateIosAppFailedFuture()); + + try { + projectManagement.createIosAppAsync(TEST_APP_BUNDLE_ID, TEST_APP_DISPLAY_NAME).get(); + fail("createIosAppAsync (with display name) did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + @Test + public void testListIosAppsShouldSucceed() throws Exception { + IosApp app1 = new IosApp(TEST_APP_ID, iosAppService); + IosApp app2 = new IosApp(TEST_APP_ID_2, iosAppService); + IosApp app3 = new IosApp(TEST_APP_ID_3, iosAppService); + when(iosAppService.listIosApps(TEST_PROJECT_ID)).thenReturn(ImmutableList.of(app1, app2, app3)); + + List iosApps = projectManagement.listIosApps(); + + ImmutableSet.Builder appIdsBuilder = ImmutableSet.builder(); + for (IosApp iosApp : iosApps) { + appIdsBuilder.add(iosApp.getAppId()); + } + assertEquals(ImmutableSet.of(TEST_APP_ID, TEST_APP_ID_2, TEST_APP_ID_3), appIdsBuilder.build()); + } + + @Test + public void testListIosAppsShouldRethrow() throws Exception { + when(iosAppService.listIosApps(TEST_PROJECT_ID)) + .thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + try { + projectManagement.listIosApps(); + fail("listIosApps did not rethrow"); + } catch (FirebaseProjectManagementException e) { + // Pass. + } + } + + @Test + public void testListIosAppsAsyncShouldSucceed() throws Exception { + IosApp app1 = new IosApp(TEST_APP_ID, iosAppService); + IosApp app2 = new IosApp(TEST_APP_ID_2, iosAppService); + IosApp app3 = new IosApp(TEST_APP_ID_3, iosAppService); + List iosApps = ImmutableList.of(app1, app2, app3); + when(iosAppService.listIosAppsAsync(TEST_PROJECT_ID)).thenReturn(immediateFuture(iosApps)); + + List actualIosApps = projectManagement.listIosAppsAsync().get(); + + ImmutableSet.Builder appIdsBuilder = ImmutableSet.builder(); + for (IosApp iosApp : actualIosApps) { + appIdsBuilder.add(iosApp.getAppId()); + } + assertEquals(ImmutableSet.of(TEST_APP_ID, TEST_APP_ID_2, TEST_APP_ID_3), appIdsBuilder.build()); + } + + @Test + public void testListIosAppsAsyncShouldRethrow() throws Exception { + when(iosAppService.listIosAppsAsync(TEST_PROJECT_ID)) + .thenReturn(immediateListIosAppFailedFuture()); + + try { + projectManagement.listIosAppsAsync().get(); + fail("listIosAppsAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + private ApiFuture immediateAndroidAppFailedFuture() { + return ApiFutures.immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } + + private ApiFuture immediateIosAppFailedFuture() { + return ApiFutures.immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } + + private ApiFuture> immediateListAndroidAppFailedFuture() { + return ApiFutures.>immediateFailedFuture( + FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } + + private ApiFuture> immediateListIosAppFailedFuture() { + return ApiFutures.>immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java new file mode 100644 index 000000000..fbc4fc8ad --- /dev/null +++ b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static com.google.api.core.ApiFutures.immediateFuture; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class IosAppTest { + + private static final String TEST_APP_NAME = + "projects/hello-world/iosApps/1:1234567890:ios:cafef00ddeadbeef"; + private static final String TEST_APP_ID = "1:1234567890:ios:cafef00ddeadbeef"; + private static final String TEST_APP_DISPLAY_NAME = "Hello World!"; + private static final String NEW_DISPLAY_NAME = "Hello?"; + private static final String TEST_PROJECT_ID = "hello-world"; + private static final String TEST_APP_BUNDLE_ID = "com.hello.world"; + private static final String TEST_APP_CONFIG = + "" + + "SOME_KEYsome-value" + + "SOME_OTHER_KEYsome-other-value" + + ""; + private static final FirebaseProjectManagementException FIREBASE_PROJECT_MANAGEMENT_EXCEPTION = + new FirebaseProjectManagementException("Error!", null); + + private static final IosAppMetadata TEST_IOS_APP_METADATA = new IosAppMetadata( + TEST_APP_NAME, + TEST_APP_ID, + TEST_APP_DISPLAY_NAME, + TEST_PROJECT_ID, + TEST_APP_BUNDLE_ID); + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock(answer = Answers.RETURNS_SMART_NULLS) + private IosAppService iosAppService; + + private IosApp iosApp; + + @Before + public void setUp() { + iosApp = new IosApp(TEST_APP_ID, iosAppService); + } + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetIosApp() throws Exception { + assertEquals(TEST_APP_ID, iosApp.getAppId()); + } + + @Test + public void testGetIosAppShouldSucceed() throws Exception { + when(iosAppService.getIosApp(TEST_APP_ID)).thenReturn(TEST_IOS_APP_METADATA); + + assertEquals(TEST_IOS_APP_METADATA, iosApp.getMetadata()); + } + + @Test + public void testGetIosAppShouldRethrow() throws Exception { + when(iosAppService.getIosApp(TEST_APP_ID)).thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + thrown.expect(FirebaseProjectManagementException.class); + + iosApp.getMetadata(); + } + + @Test + public void testGetIosAppAsyncShouldSucceed() throws Exception { + when(iosAppService.getIosAppAsync(TEST_APP_ID)) + .thenReturn(immediateFuture(TEST_IOS_APP_METADATA)); + + assertEquals(TEST_IOS_APP_METADATA, iosApp.getMetadataAsync().get()); + } + + @Test + public void testGetIosAppAsyncShouldRethrow() throws Exception { + when(iosAppService.getIosAppAsync(TEST_APP_ID)) + .thenReturn(immediateIosAppMetadataFailedFuture()); + + try { + iosApp.getMetadataAsync().get(); + fail("getMetadataAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + @Test + public void testSetDisplayNameShouldSucceed() throws Exception { + iosApp.setDisplayName(NEW_DISPLAY_NAME); + + verify(iosAppService).setIosDisplayName(TEST_APP_ID, NEW_DISPLAY_NAME); + } + + @Test + public void testSetDisplayNameShouldRethrow() throws Exception { + doThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION) + .when(iosAppService) + .setIosDisplayName(TEST_APP_ID, NEW_DISPLAY_NAME); + + thrown.expect(FirebaseProjectManagementException.class); + + iosApp.setDisplayName(NEW_DISPLAY_NAME); + } + + @Test + public void testSetDisplayNameAsyncShouldSucceed() throws Exception { + when(iosAppService.setIosDisplayNameAsync(TEST_APP_ID, NEW_DISPLAY_NAME)) + .thenReturn(immediateFuture((Void) null)); + + iosApp.setDisplayNameAsync(NEW_DISPLAY_NAME).get(); + } + + @Test + public void testSetDisplayNameAsyncShouldRethrow() throws Exception { + when(iosAppService.setIosDisplayNameAsync(TEST_APP_ID, NEW_DISPLAY_NAME)) + .thenReturn(immediateVoidFailedFuture()); + + try { + iosApp.setDisplayNameAsync(NEW_DISPLAY_NAME).get(); + fail("setDisplayNameAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + @Test + public void testGetConfigShouldSucceed() throws Exception { + when(iosAppService.getIosConfig(TEST_APP_ID)).thenReturn(TEST_APP_CONFIG); + + assertEquals(TEST_APP_CONFIG, iosApp.getConfig()); + } + + @Test + public void testGetConfigShouldRethrow() throws Exception { + when(iosAppService.getIosConfig(TEST_APP_ID)).thenThrow(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + + thrown.expect(FirebaseProjectManagementException.class); + + iosApp.getConfig(); + } + + @Test + public void testGetConfigAsyncShouldSucceed() throws Exception { + when(iosAppService.getIosConfigAsync(TEST_APP_ID)).thenReturn(immediateFuture(TEST_APP_CONFIG)); + + assertEquals(TEST_APP_CONFIG, iosApp.getConfigAsync().get()); + } + + @Test + public void testGetConfigAsyncShouldRethrow() throws Exception { + when(iosAppService.getIosConfigAsync(TEST_APP_ID)).thenReturn(immediateStringFailedFuture()); + + try { + iosApp.getConfigAsync().get(); + fail("getConfigAsync did not rethrow"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseProjectManagementException); + } + } + + private ApiFuture immediateIosAppMetadataFailedFuture() { + return ApiFutures.immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } + + private ApiFuture immediateVoidFailedFuture() { + return ApiFutures.immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } + + private ApiFuture immediateStringFailedFuture() { + return ApiFutures.immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java b/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java new file mode 100644 index 000000000..f5ef0911e --- /dev/null +++ b/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.projectmanagement; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ShaCertificateTest { + + @Test + public void getTypeFromHashSha1() { + assertEquals( + ShaCertificate.getTypeFromHash("1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"), + ShaCertificateType.SHA_1); + } + + @Test(expected = IllegalArgumentException.class) + public void getTypeFromHashSha1WithSpecialCharacter() { + ShaCertificate.getTypeFromHash("&111AAAA1111AAAA1111AAAA1111AAAA1111AAA$"); + } + + @Test(expected = IllegalArgumentException.class) + public void getTypeFromHashSha1WithIncorrectSize() { + ShaCertificate.getTypeFromHash("1111AAAA1111AAAA1111AAAA1111AAAA1111"); + } + + @Test + public void getTypeFromHashSha256() { + assertEquals( + ShaCertificate.getTypeFromHash( + "1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"), + ShaCertificateType.SHA_256); + } + + @Test(expected = IllegalArgumentException.class) + public void getTypeFromHashSha256WithSpecialCharacter() { + ShaCertificate.getTypeFromHash( + "&111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAA&"); + } + + @Test(expected = IllegalArgumentException.class) + public void getTypeFromHashSha256WithIncorrectSize() { + ShaCertificate.getTypeFromHash( + "1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"); + } +} From 32f05c327fa49ffa9f0ca393adbc4fa3bf788cc6 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 26 Nov 2018 16:27:27 -0800 Subject: [PATCH 018/441] Migrating Auth API to the new Identity Toolkit endpoint (#220) * Migrating Auth API to the new Identity Toolkit endpoint * Updated CHANGELOG --- CHANGELOG.md | 2 + .../firebase/auth/FirebaseUserManager.java | 54 +++++++++++++------ .../firebase/auth/FirebaseAuthTest.java | 6 ++- .../auth/FirebaseUserManagerTest.java | 39 ++++++++------ 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b08e7c289..96fad109d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ code. - [fixed] FCM errors sent by the back-end now include more details that are helpful when debugging problems. +- [changed] Migrated the `FirebaseAuth` user management API to the + new Identity Toolkit endpoint. # v6.5.0 diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 39b3e3780..294f10a0f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; @@ -36,6 +37,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.DownloadAccountResponse; @@ -45,7 +47,9 @@ import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.FirebaseRequestInitializer; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; import com.google.firebase.internal.SdkUtils; + import java.io.IOException; import java.util.List; import java.util.Map; @@ -91,9 +95,10 @@ class FirebaseUserManager { "iss", "jti", "nbf", "nonce", "sub", "firebase"); private static final String ID_TOOLKIT_URL = - "https://www.googleapis.com/identitytoolkit/v3/relyingparty/"; + "https://identitytoolkit.googleapis.com/v1/projects/%s"; private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + private final String baseUrl; private final JsonFactory jsonFactory; private final HttpRequestFactory requestFactory; private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); @@ -107,6 +112,12 @@ class FirebaseUserManager { */ FirebaseUserManager(@NonNull FirebaseApp app) { checkNotNull(app, "FirebaseApp must not be null"); + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access the auth service. Use a service account credential or " + + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + this.baseUrl = String.format(ID_TOOLKIT_URL, projectId); this.jsonFactory = app.getOptions().getJsonFactory(); HttpTransport transport = app.getOptions().getHttpTransport(); this.requestFactory = transport.createRequestFactory(new FirebaseRequestInitializer(app)); @@ -121,7 +132,7 @@ UserRecord getUserById(String uid) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "localId", ImmutableList.of(uid)); GetAccountInfoResponse response = post( - "getAccountInfo", payload, GetAccountInfoResponse.class); + "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, "No user record found for the provided user ID: " + uid); @@ -133,7 +144,7 @@ UserRecord getUserByEmail(String email) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "email", ImmutableList.of(email)); GetAccountInfoResponse response = post( - "getAccountInfo", payload, GetAccountInfoResponse.class); + "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, "No user record found for the provided email: " + email); @@ -145,7 +156,7 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException final Map payload = ImmutableMap.of( "phoneNumber", ImmutableList.of(phoneNumber)); GetAccountInfoResponse response = post( - "getAccountInfo", payload, GetAccountInfoResponse.class); + "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, "No user record found for the provided phone number: " + phoneNumber); @@ -155,7 +166,7 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException String createUser(CreateRequest request) throws FirebaseAuthException { GenericJson response = post( - "signupNewUser", request.getProperties(), GenericJson.class); + "/accounts", request.getProperties(), GenericJson.class); if (response != null) { String uid = (String) response.get("localId"); if (!Strings.isNullOrEmpty(uid)) { @@ -167,7 +178,7 @@ String createUser(CreateRequest request) throws FirebaseAuthException { void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException { GenericJson response = post( - "setAccountInfo", request.getProperties(jsonFactory), GenericJson.class); + "/accounts:update", request.getProperties(jsonFactory), GenericJson.class); if (response == null || !request.getUid().equals(response.get("localId"))) { throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to update user: " + request.getUid()); } @@ -176,7 +187,7 @@ void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseA void deleteUser(String uid) throws FirebaseAuthException { final Map payload = ImmutableMap.of("localId", uid); GenericJson response = post( - "deleteAccount", payload, GenericJson.class); + "/accounts:delete", payload, GenericJson.class); if (response == null || !response.containsKey("kind")) { throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete user: " + uid); } @@ -190,8 +201,10 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb builder.put("nextPageToken", pageToken); } - DownloadAccountResponse response = post( - "downloadAccount", builder.build(), DownloadAccountResponse.class); + GenericUrl url = new GenericUrl(baseUrl + "/accounts:batchGet"); + url.putAll(builder.build()); + DownloadAccountResponse response = sendRequest( + "GET", url, null, DownloadAccountResponse.class); if (response == null) { throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve users."); } @@ -200,7 +213,8 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException { checkNotNull(request); - UploadAccountResponse response = post("uploadAccount", request, UploadAccountResponse.class); + UploadAccountResponse response = post( + "/accounts:batchCreate", request, UploadAccountResponse.class); if (response == null) { throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users."); } @@ -211,7 +225,7 @@ String createSessionCookie(String idToken, SessionCookieOptions options) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "idToken", idToken, "validDuration", options.getExpiresInSeconds()); - GenericJson response = post("createSessionCookie", payload, GenericJson.class); + GenericJson response = post(":createSessionCookie", payload, GenericJson.class); if (response != null) { String cookie = (String) response.get("sessionCookie"); if (!Strings.isNullOrEmpty(cookie)) { @@ -223,14 +237,22 @@ String createSessionCookie(String idToken, private T post(String path, Object content, Class clazz) throws FirebaseAuthException { checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty"); - checkNotNull(content, "content must not be null"); - checkNotNull(clazz, "response class must not be null"); + checkNotNull(content, "content must not be null for POST requests"); + GenericUrl url = new GenericUrl(baseUrl + path); + return sendRequest("POST", url, content, clazz); + } - GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + path); + private T sendRequest( + String method, GenericUrl url, + @Nullable Object content, Class clazz) throws FirebaseAuthException { + + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); + checkNotNull(url, "url must not be null"); + checkNotNull(clazz, "response class must not be null"); HttpResponse response = null; try { - HttpRequest request = requestFactory.buildPostRequest(url, - new JsonHttpContent(jsonFactory, content)); + HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; + HttpRequest request = requestFactory.buildRequest(method, url, httpContent); request.setParser(new JsonObjectParser(jsonFactory)); request.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion); request.setResponseInterceptor(interceptor); diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index db8a03047..a4c2dc533 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -36,7 +36,6 @@ import com.google.auth.oauth2.UserCredentials; import com.google.common.base.Defaults; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -98,7 +97,10 @@ public static Collection data() throws Exception { /* isCertCredential */ true }, { - new FirebaseOptions.Builder().setCredentials(createRefreshTokenCredential()).build(), + new FirebaseOptions.Builder() + .setCredentials(createRefreshTokenCredential()) + .setProjectId("test-project-id") + .build(), /* isCertCredential */ false }, { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index df5a34e2d..fe6f00071 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; @@ -67,6 +68,18 @@ public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } + @Test + public void testProjectIdRequired() { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance(); + fail("No error thrown for missing project ID"); + } catch (IllegalArgumentException expected) { + } + } + @Test public void testGetUser() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( @@ -149,12 +162,9 @@ public void testListUsers() throws Exception { assertEquals("", page.getNextPageToken()); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); - assertEquals(new BigDecimal(999), parsed.get("maxResults")); - assertNull(parsed.get("nextPageToken")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(999, url.getFirst("maxResults")); + assertNull(url.getFirst("nextPageToken")); } @Test @@ -171,12 +181,9 @@ public void testListUsersWithPageToken() throws Exception { assertEquals("", page.getNextPageToken()); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); - assertEquals(new BigDecimal(999), parsed.get("maxResults")); - assertEquals("token", parsed.get("nextPageToken")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(999, url.getFirst("maxResults")); + assertEquals("token", url.getFirst("nextPageToken")); } @Test @@ -427,9 +434,7 @@ public void testCreateSessionCookie() throws Exception { @Test public void testCreateSessionCookieInvalidArguments() { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() - .setCredentials(credentials) - .build()); + initializeAppForUserManagement(); SessionCookieOptions options = SessionCookieOptions.builder() .setExpiresIn(TimeUnit.HOURS.toMillis(1)) .build(); @@ -532,6 +537,7 @@ public void call(FirebaseAuth auth) throws Exception { .build(); FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(credentials) + .setProjectId("test-project-id") .setHttpTransport(transport) .build()); @@ -596,6 +602,7 @@ public void testGetUserUnexpectedHttpError() throws Exception { .build(); FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(credentials) + .setProjectId("test-project-id") .setHttpTransport(transport) .build()); try { @@ -617,6 +624,7 @@ public void testTimeout() throws Exception { new MockLowLevelHttpResponse().setContent(TestUtils.loadResource("getUser.json")))); FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(credentials) + .setProjectId("test-project-id") .setHttpTransport(transport) .setConnectTimeout(30000) .setReadTimeout(60000) @@ -989,6 +997,7 @@ private static TestResponseInterceptor initializeAppForUserManagement(String ... FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(credentials) .setHttpTransport(transport) + .setProjectId("test-project-id") .build()); FirebaseAuth auth = FirebaseAuth.getInstance(); FirebaseUserManager userManager = auth.getUserManager(); From 3bade2b59959f1e4a8634e65b4066a15529c277a Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 27 Nov 2018 22:02:52 +0100 Subject: [PATCH 019/441] Add subtitle in ApsAlert (#219) * Add subtitle field in ApsAlert payload + tests * fix checkstyle * fix test * fix checkstyle * fix test * fix test * fix test * update test --- .../google/firebase/messaging/ApsAlert.java | 68 +++++++++++++++++++ .../messaging/FirebaseMessagingTest.java | 4 +- .../firebase/messaging/MessageTest.java | 10 ++- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/ApsAlert.java b/src/main/java/com/google/firebase/messaging/ApsAlert.java index 6f7e249f3..2efa94433 100644 --- a/src/main/java/com/google/firebase/messaging/ApsAlert.java +++ b/src/main/java/com/google/firebase/messaging/ApsAlert.java @@ -34,6 +34,9 @@ public class ApsAlert { @Key("title") private final String title; + @Key("subtitle") + private final String subtitle; + @Key("body") private final String body; @@ -49,6 +52,12 @@ public class ApsAlert { @Key("title-loc-args") private final List titleLocArgs; + @Key("subtitle-loc-key") + private final String subtitleLocKey; + + @Key("subtitle-loc-args") + private final List subtitleLocArgs; + @Key("action-loc-key") private final String actionLocKey; @@ -57,6 +66,7 @@ public class ApsAlert { private ApsAlert(Builder builder) { this.title = builder.title; + this.subtitle = builder.subtitle; this.body = builder.body; this.actionLocKey = builder.actionLocKey; this.locKey = builder.locKey; @@ -76,6 +86,14 @@ private ApsAlert(Builder builder) { } else { this.titleLocArgs = null; } + this.subtitleLocKey = builder.subtitleLocKey; + if (!builder.subtitleLocArgs.isEmpty()) { + checkArgument(!Strings.isNullOrEmpty(builder.subtitleLocKey), + "subtitleLocKey is required when specifying subtitleLocArgs"); + this.subtitleLocArgs = ImmutableList.copyOf(builder.subtitleLocArgs); + } else { + this.subtitleLocArgs = null; + } this.launchImage = builder.launchImage; } @@ -91,11 +109,14 @@ public static Builder builder() { public static class Builder { private String title; + private String subtitle; private String body; private String locKey; private List locArgs = new ArrayList<>(); private String titleLocKey; private List titleLocArgs = new ArrayList<>(); + private String subtitleLocKey; + private List subtitleLocArgs = new ArrayList<>(); private String actionLocKey; private String launchImage; @@ -113,6 +134,17 @@ public Builder setTitle(String title) { return this; } + /** + * Sets the subtitle of the alert. + * + * @param subtitle Subtitle of the notification. + * @return This builder. + */ + public Builder setSubtitle(String subtitle) { + this.subtitle = subtitle; + return this; + } + /** * Sets the body of the alert. When provided, overrides the body sent * via {@link Notification}. @@ -209,6 +241,42 @@ public Builder addAllTitleLocArgs(@NonNull List args) { return this; } + /** + * Sets the key of the subtitle string in the app's string resources to use to localize + * the subtitle text. + * + * @param subtitleLocKey Resource key string. + * @return This builder. + */ + public Builder setSubtitleLocalizationKey(String subtitleLocKey) { + this.subtitleLocKey = subtitleLocKey; + return this; + } + + /** + * Adds a resource key string that will be used in place of the format specifiers in + * {@code subtitleLocKey}. + * + * @param arg Resource key string. + * @return This builder. + */ + public Builder addSubtitleLocalizationArg(@NonNull String arg) { + this.subtitleLocArgs.add(arg); + return this; + } + + /** + * Adds a list of resource keys that will be used in place of the format specifiers in + * {@code subtitleLocKey}. + * + * @param args List of resource key strings. + * @return This builder. + */ + public Builder addAllSubtitleLocArgs(@NonNull List args) { + this.subtitleLocArgs.addAll(args); + return this; + } + /** * Sets the launch image for the notification action. * diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 4b51513be..1ad74ac33 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -644,6 +644,7 @@ private static Map> buildTestMessages() { .setBadge(42) .setAlert(ApsAlert.builder() .setTitle("test-title") + .setSubtitle("test-subtitle") .setBody("test-body") .build()) .build()) @@ -657,7 +658,8 @@ private static Map> buildTestMessages() { "payload", ImmutableMap.of("k1", "v1", "k2", true, "aps", ImmutableMap.of("badge", new BigDecimal(42), "alert", ImmutableMap.of( - "title", "test-title", "body", "test-body")))) + "title", "test-title", "subtitle", "test-subtitle", + "body", "test-body")))) )); // Webpush message (no notification) diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 3313f2bb3..b6025c0bb 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -393,14 +393,18 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { .setAps(Aps.builder() .setAlert(ApsAlert.builder() .setTitle("test-title") + .setSubtitle("test-subtitle") .setBody("test-body") .setLocalizationKey("test-loc-key") .setActionLocalizationKey("test-action-loc-key") .setTitleLocalizationKey("test-title-loc-key") + .setSubtitleLocalizationKey("test-subtitle-loc-key") .addLocalizationArg("arg1") .addAllLocalizationArgs(ImmutableList.of("arg2", "arg3")) .addTitleLocalizationArg("arg4") .addAllTitleLocArgs(ImmutableList.of("arg5", "arg6")) + .addSubtitleLocalizationArg("arg7") + .addAllSubtitleLocArgs(ImmutableList.of("arg8", "arg9")) .setLaunchImage("test-image") .build()) .setCategory("test-category") @@ -417,12 +421,15 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { "aps", ImmutableMap.builder() .put("alert", ImmutableMap.builder() .put("title", "test-title") + .put("subtitle", "test-subtitle") .put("body", "test-body") .put("loc-key", "test-loc-key") .put("action-loc-key", "test-action-loc-key") .put("title-loc-key", "test-title-loc-key") + .put("subtitle-loc-key", "test-subtitle-loc-key") .put("loc-args", ImmutableList.of("arg1", "arg2", "arg3")) .put("title-loc-args", ImmutableList.of("arg4", "arg5", "arg6")) + .put("subtitle-loc-args", ImmutableList.of("arg7", "arg8", "arg9")) .put("launch-image", "test-image") .build()) .put("category", "test-category") @@ -474,7 +481,8 @@ public void testInvalidApnsConfig() { List notificationBuilders = ImmutableList.of( ApsAlert.builder().addLocalizationArg("foo"), - ApsAlert.builder().addTitleLocalizationArg("foo") + ApsAlert.builder().addTitleLocalizationArg("foo"), + ApsAlert.builder().addSubtitleLocalizationArg("foo") ); for (int i = 0; i < notificationBuilders.size(); i++) { try { From aacafecc8445c8ffb8b3a6b026af60b9fbd1fde7 Mon Sep 17 00:00:00 2001 From: weixifan <44070836+weixifan@users.noreply.github.com> Date: Tue, 27 Nov 2018 16:41:16 -0500 Subject: [PATCH 020/441] Make adjustments to the public API as discussed offline (#221) --- .../projectmanagement/AndroidAppMetadata.java | 9 ++- .../FirebaseProjectManagementServiceImpl.java | 4 +- .../projectmanagement/IosAppMetadata.java | 9 ++- ...ebaseProjectManagementServiceImplTest.java | 67 +++++++++++++++++++ .../FirebaseProjectManagementTest.java | 1 - 5 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java b/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java index f60a0b035..055994d51 100644 --- a/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java +++ b/src/main/java/com/google/firebase/projectmanagement/AndroidAppMetadata.java @@ -18,6 +18,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.firebase.internal.Nullable; /** * Contains detailed information about an Android App. Instances of this class are immutable. @@ -34,7 +35,7 @@ public class AndroidAppMetadata { String name, String appId, String displayName, String projectId, String packageName) { this.name = Preconditions.checkNotNull(name, "Null name"); this.appId = Preconditions.checkNotNull(appId, "Null appId"); - this.displayName = Preconditions.checkNotNull(displayName, "Null displayName"); + this.displayName = displayName; this.projectId = Preconditions.checkNotNull(projectId, "Null projectId"); this.packageName = Preconditions.checkNotNull(packageName, "Null packageName"); } @@ -42,7 +43,7 @@ public class AndroidAppMetadata { /** * Returns the fully qualified resource name of this Android App. */ - public String getName() { + String getName() { return name; } @@ -55,8 +56,10 @@ public String getAppId() { } /** - * Returns the user-assigned display name of this Android App. + * Returns the user-assigned display name of this Android App. Returns {@code null} if it has + * never been set. */ + @Nullable public String getDisplayName() { return displayName; } diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 474b8b0cf..6ce7f3514 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -103,7 +103,7 @@ protected AndroidAppMetadata execute() throws FirebaseProjectManagementException return new AndroidAppMetadata( parsedResponse.name, parsedResponse.appId, - Strings.nullToEmpty(parsedResponse.displayName), + Strings.emptyToNull(parsedResponse.displayName), parsedResponse.projectId, parsedResponse.packageName); } @@ -134,7 +134,7 @@ protected IosAppMetadata execute() throws FirebaseProjectManagementException { return new IosAppMetadata( parsedResponse.name, parsedResponse.appId, - Strings.nullToEmpty(parsedResponse.displayName), + Strings.emptyToNull(parsedResponse.displayName), parsedResponse.projectId, parsedResponse.bundleId); } diff --git a/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java b/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java index bbd8167b4..5de66f7ac 100644 --- a/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java +++ b/src/main/java/com/google/firebase/projectmanagement/IosAppMetadata.java @@ -18,6 +18,7 @@ import com.google.api.client.util.Preconditions; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.firebase.internal.Nullable; /** * Contains detailed information about an iOS App. Instances of this class are immutable. @@ -33,7 +34,7 @@ public class IosAppMetadata { String name, String appId, String displayName, String projectId, String bundleId) { this.name = Preconditions.checkNotNull(name, "Null name"); this.appId = Preconditions.checkNotNull(appId, "Null appId"); - this.displayName = Preconditions.checkNotNull(displayName, "Null displayName"); + this.displayName = displayName; this.projectId = Preconditions.checkNotNull(projectId, "Null projectId"); this.bundleId = Preconditions.checkNotNull(bundleId, "Null bundleId"); } @@ -41,7 +42,7 @@ public class IosAppMetadata { /** * Returns the fully qualified resource name of this iOS App. */ - public String getName() { + String getName() { return name; } @@ -54,8 +55,10 @@ public String getAppId() { } /** - * Returns the user-assigned display name of this iOS App. + * Returns the user-assigned display name of this iOS App. Returns {@code null} if it has never + * been set. */ + @Nullable public String getDisplayName() { return displayName; } diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index 5ba86b28e..45b51a227 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -70,6 +70,11 @@ public class FirebaseProjectManagementServiceImplTest { private static final AndroidAppMetadata ANDROID_APP_METADATA = new AndroidAppMetadata( ANDROID_APP_RESOURCE_NAME, ANDROID_APP_ID, DISPLAY_NAME, PROJECT_ID, PACKAGE_NAME); + private static final IosAppMetadata IOS_APP_NO_DISPLAY_NAME_METADATA = + new IosAppMetadata(IOS_APP_RESOURCE_NAME, IOS_APP_ID, null, PROJECT_ID, BUNDLE_ID); + private static final AndroidAppMetadata ANDROID_APP_NO_DISPLAY_NAME_METADATA = + new AndroidAppMetadata( + ANDROID_APP_RESOURCE_NAME, ANDROID_APP_ID, null, PROJECT_ID, PACKAGE_NAME); private static final String IOS_CONFIG_CONTENT = "ios-config-content"; private static final String ANDROID_CONFIG_CONTENT = "android-config-content"; @@ -100,6 +105,11 @@ public class FirebaseProjectManagementServiceImplTest { + "\"displayName\" : \"%s\", " + "\"projectId\" : \"test-project-id\", " + "\"bundleId\" : \"test.ios.app\"}"; + private static final String GET_IOS_NO_DISPLAY_NAME_RESPONSE = + "{\"name\" : \"ios/11111\", " + + "\"appId\" : \"test-ios-app-id\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app\"}"; private static final String GET_IOS_CONFIG_RESPONSE = "{\"configFilename\" : \"test-ios-app-config-name\", " + "\"configFileContents\" : \"ios-config-content\"}"; @@ -154,6 +164,11 @@ public class FirebaseProjectManagementServiceImplTest { + "\"displayName\" : \"%s\", " + "\"projectId\" : \"test-project-id\", " + "\"packageName\" : \"test.android.app\"}"; + private static final String GET_ANDROID_NO_DISPLAY_NAME_RESPONSE = + "{\"name\" : \"android/11111\", " + + "\"appId\" : \"test-android-app-id\", " + + "\"projectId\" : \"test-project-id\", " + + "\"packageName\" : \"test.android.app\"}"; private static final String GET_ANDROID_CONFIG_RESPONSE = "{\"configFilename\" : \"test-android-app-config-name\", " + "\"configFileContents\" : \"android-config-content\"}"; @@ -243,6 +258,32 @@ public void getIosAppAsync() throws Exception { assertEquals(iosAppMetadata, IOS_APP_METADATA); } + @Test + public void getIosAppNoDisplayName() throws Exception { + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); + firstRpcResponse.setContent(GET_IOS_NO_DISPLAY_NAME_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + IosAppMetadata iosAppMetadata = serviceImpl.getIosApp(IOS_APP_ID); + + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(iosAppMetadata, IOS_APP_NO_DISPLAY_NAME_METADATA); + } + + @Test + public void getIosAppAsyncNoDisplayName() throws Exception { + String expectedUrl = String.format( + "%s/v1beta1/projects/-/iosApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); + firstRpcResponse.setContent(GET_IOS_NO_DISPLAY_NAME_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + IosAppMetadata iosAppMetadata = serviceImpl.getIosAppAsync(IOS_APP_ID).get(); + + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(iosAppMetadata, IOS_APP_NO_DISPLAY_NAME_METADATA); + } + @Test public void listIosApps() throws Exception { String expectedUrl = String.format( @@ -485,6 +526,32 @@ public void getAndroidAppAsync() throws Exception { assertEquals(androidAppMetadata, ANDROID_APP_METADATA); } + @Test + public void getAndroidAppNoDisplayName() throws Exception { + firstRpcResponse.setContent(GET_ANDROID_NO_DISPLAY_NAME_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + AndroidAppMetadata androidAppMetadata = serviceImpl.getAndroidApp(ANDROID_APP_ID); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(androidAppMetadata, ANDROID_APP_NO_DISPLAY_NAME_METADATA); + } + + @Test + public void getAndroidAppAsyncNoDisplayName() throws Exception { + firstRpcResponse.setContent(GET_ANDROID_NO_DISPLAY_NAME_RESPONSE); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + AndroidAppMetadata androidAppMetadata = serviceImpl.getAndroidAppAsync(ANDROID_APP_ID).get(); + + String expectedUrl = String.format( + "%s/v1beta1/projects/-/androidApps/%s", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); + checkRequestHeader(expectedUrl, HttpMethod.GET); + assertEquals(androidAppMetadata, ANDROID_APP_NO_DISPLAY_NAME_METADATA); + } + @Test public void listAndroidApps() throws Exception { firstRpcResponse.setContent(LIST_ANDROID_APPS_RESPONSE); diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java index 3d51eecad..7569b0bc2 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java @@ -32,7 +32,6 @@ import com.google.firebase.auth.MockGoogleCredentials; import java.util.List; import java.util.concurrent.ExecutionException; -import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; From a81bc87aa383610f8c86d674d63387f9aed9f7ed Mon Sep 17 00:00:00 2001 From: weixifan <44070836+weixifan@users.noreply.github.com> Date: Tue, 27 Nov 2018 17:05:30 -0500 Subject: [PATCH 021/441] Fix a NullPointerException in the FirebaseProjectManagementIT (#222) * Fix a NullPointerException in the FirebaseProjectManagementIT. --- .../projectmanagement/FirebaseProjectManagementIT.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java index b6dd7a40d..976a0d53d 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementIT.java @@ -49,7 +49,8 @@ public static void setUpClass() throws Exception { // Ensure that we have created a Test iOS App. List iosApps = projectManagement.listIosApps(); for (IosApp iosApp : iosApps) { - if (iosApp.getMetadata().getDisplayName().startsWith(TEST_APP_DISPLAY_NAME_PREFIX)) { + if (Strings.nullToEmpty(iosApp.getMetadata().getDisplayName()) + .startsWith(TEST_APP_DISPLAY_NAME_PREFIX)) { testIosAppId = iosApp.getAppId(); } } @@ -61,7 +62,8 @@ public static void setUpClass() throws Exception { // Ensure that we have created a Test Android App. List androidApps = projectManagement.listAndroidApps(); for (AndroidApp androidApp : androidApps) { - if (androidApp.getMetadata().getDisplayName().startsWith(TEST_APP_DISPLAY_NAME_PREFIX)) { + if (Strings.nullToEmpty(androidApp.getMetadata().getDisplayName()) + .startsWith(TEST_APP_DISPLAY_NAME_PREFIX)) { testAndroidAppId = androidApp.getAppId(); } } From 83daddf0266bedf1dbe02a22286c6dd02c9595ed Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 28 Nov 2018 14:57:58 -0800 Subject: [PATCH 022/441] Staged Release v6.6.0 (#223) * Updating CHANGELOG for 6.6.0 release. * [maven-release-plugin] prepare release v6.6.0 * [maven-release-plugin] prepare for next development iteration * Updated changelog --- CHANGELOG.md | 6 ++++++ pom.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fad109d..a8bd41ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Unreleased +- + +# v6.6.0 + +- [added] Added a new `FirebaseProjectManagement` API for managing + apps in a Firebase project. - [fixed] Fixing error handling in FCM. The SDK now checks the key `type.googleapis.com/google.firebase.fcm.v1.FcmError` to set error code. diff --git a/pom.xml b/pom.xml index 9269f3b5a..fbd144d03 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.5.1-SNAPSHOT + 6.6.1-SNAPSHOT jar firebase-admin From 4e7cfc63a684d6c87f3ba44192374e488e8f054c Mon Sep 17 00:00:00 2001 From: weixifan <44070836+weixifan@users.noreply.github.com> Date: Wed, 28 Nov 2018 18:04:20 -0500 Subject: [PATCH 023/441] Fix documentation issues. (#225) --- .../firebase/projectmanagement/AndroidApp.java | 8 ++++---- .../firebase/projectmanagement/ShaCertificate.java | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java b/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java index d1af6db51..dd7a00e15 100644 --- a/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java +++ b/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java @@ -120,7 +120,7 @@ public ApiFuture> getShaCertificatesAsync() { } /** - * Adds a SHA certificate to this Android app. + * Adds the given SHA certificate to this Android app. * * @param certificateToAdd the SHA certificate to be added to this Android app * @return a {@link ShaCertificate} that was created for this Android app, containing resource @@ -133,7 +133,7 @@ public ShaCertificate createShaCertificate(ShaCertificate certificateToAdd) } /** - * Asynchronously adds a SHA certificate to this Android app. + * Asynchronously adds the given SHA certificate to this Android app. * * @param certificateToAdd the SHA certificate to be added to this Android app * @return a {@code ApiFuture} of a {@link ShaCertificate} that was created for this Android app, @@ -144,7 +144,7 @@ public ApiFuture createShaCertificateAsync(ShaCertificate certif } /** - * Removes a SHA certificate from this Android app. + * Removes the given SHA certificate from this Android app. * * @param certificateToRemove the SHA certificate to be removed from this Android app * @throws FirebaseProjectManagementException if there was an error during the RPC @@ -155,7 +155,7 @@ public void deleteShaCertificate(ShaCertificate certificateToRemove) } /** - * Asynchronously removes a SHA certificate from this Android app. + * Asynchronously removes the given SHA certificate from this Android app. * * @param certificateToRemove the SHA certificate to be removed from this Android app */ diff --git a/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java b/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java index 805ac3dad..d587f8153 100644 --- a/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java +++ b/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java @@ -23,7 +23,7 @@ import java.util.regex.Pattern; /** - * Contains detailed information of a SHA certificate, which can be associated to an Android app. + * Information about an SHA certificate associated with an Android app. */ public class ShaCertificate { @@ -41,11 +41,13 @@ private ShaCertificate(String name, String shaHash, ShaCertificateType certType) } /** - * Creates a {@link ShaCertificate} from certificate hash. Name will be left as empty string since - * the certificate doesn't have a generated name yet. + * Creates an {@link ShaCertificate} from the given certificate hash. + * + *

The fully qualified resource name of this certificate will be set to the empty string since + * it has not been generated yet. * * @param shaHash SHA hash of the certificate - * @return a SHA certificate + * @return a new {@link ShaCertificate} instance */ public static ShaCertificate create(String shaHash) { return new ShaCertificate("", shaHash, getTypeFromHash(shaHash)); @@ -69,7 +71,7 @@ static ShaCertificateType getTypeFromHash(String shaHash) { } else if (SHA256_PATTERN.matcher(shaHash).matches()) { return ShaCertificateType.SHA_256; } - throw new IllegalArgumentException("Invalid SHA hash, it is neither SHA-1 nor SHA-256."); + throw new IllegalArgumentException("Invalid SHA hash; it is neither SHA-1 nor SHA-256."); } /** @@ -87,7 +89,7 @@ public String getShaHash() { } /** - * Returns the type {@link ShaCertificateType} of this SHA certificate. + * Returns the type of this SHA certificate. */ public ShaCertificateType getCertType() { return certType; From ea9a28ab62c523b33c2290a1a7b30986ffdad28a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 29 Nov 2018 10:29:30 -0800 Subject: [PATCH 024/441] Cleaning up some Firebase Auth Unit Tests (#226) * Cleaning up some auth tests * Updated tests * Cleaned up custom token tests --- .../com/google/firebase/FirebaseAppTest.java | 32 ++- .../firebase/auth/FirebaseAuthTest.java | 232 ++---------------- .../auth/FirebaseCustomTokenTest.java | 163 ++++++++++++ 3 files changed, 201 insertions(+), 226 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index 6cc0b7103..c7036b4f0 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -448,6 +449,19 @@ public void testTokenRefresherStateMachine() { assertEquals(0, refresher.cancelCalls); } + @Test + public void testAppWithAuthVariableOverrides() { + Map authVariableOverrides = ImmutableMap.of("uid", "uid1"); + FirebaseOptions options = + new FirebaseOptions.Builder(getMockCredentialOptions()) + .setDatabaseAuthVariableOverride(authVariableOverrides) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options, "testGetAppWithUid"); + assertEquals("uid1", app.getOptions().getDatabaseAuthVariableOverride().get("uid")); + String token = TestOnlyImplFirebaseTrampolines.getToken(app, false); + Assert.assertTrue(!token.isEmpty()); + } + @Test(expected = IllegalArgumentException.class) public void testEmptyFirebaseConfigFile() { setFirebaseConfigEnvironmentVariable("firebase_config_empty.json"); @@ -458,9 +472,9 @@ public void testEmptyFirebaseConfigFile() { public void testEmptyFirebaseConfigString() { setFirebaseConfigEnvironmentVariable(""); FirebaseApp firebaseApp = FirebaseApp.initializeApp(); - assertEquals(null, firebaseApp.getOptions().getProjectId()); - assertEquals(null, firebaseApp.getOptions().getStorageBucket()); - assertEquals(null, firebaseApp.getOptions().getDatabaseUrl()); + assertNull(firebaseApp.getOptions().getProjectId()); + assertNull(firebaseApp.getOptions().getStorageBucket()); + assertNull(firebaseApp.getOptions().getDatabaseUrl()); assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); } @@ -468,9 +482,9 @@ public void testEmptyFirebaseConfigString() { public void testEmptyFirebaseConfigJSONObject() { setFirebaseConfigEnvironmentVariable("{}"); FirebaseApp firebaseApp = FirebaseApp.initializeApp(); - assertEquals(null, firebaseApp.getOptions().getProjectId()); - assertEquals(null, firebaseApp.getOptions().getStorageBucket()); - assertEquals(null, firebaseApp.getOptions().getDatabaseUrl()); + assertNull(firebaseApp.getOptions().getProjectId()); + assertNull(firebaseApp.getOptions().getStorageBucket()); + assertNull(firebaseApp.getOptions().getDatabaseUrl()); assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); } @@ -514,9 +528,9 @@ public void testValidFirebaseConfigFile() { public void testEnvironmentVariableIgnored() { setFirebaseConfigEnvironmentVariable("firebase_config.json"); FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS); - assertEquals(null, firebaseApp.getOptions().getProjectId()); - assertEquals(null, firebaseApp.getOptions().getStorageBucket()); - assertEquals(null, firebaseApp.getOptions().getDatabaseUrl()); + assertNull(firebaseApp.getOptions().getProjectId()); + assertNull(firebaseApp.getOptions().getStorageBucket()); + assertNull(firebaseApp.getOptions().getDatabaseUrl()); assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index a4c2dc533..8f19e5cac 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -19,137 +19,39 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.api.client.googleapis.testing.auth.oauth2.MockTokenServerTransport; -import com.google.api.client.googleapis.util.Utils; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.gson.GsonFactory; import com.google.api.core.ApiFuture; -import com.google.auth.http.HttpTransportFactory; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.auth.oauth2.UserCredentials; import com.google.common.base.Defaults; -import com.google.common.base.Strings; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; -import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseCustomAuthToken; -import com.google.firebase.database.MapBuilder; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.After; import org.junit.Assert; -import org.junit.Assume; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; -@RunWith(Parameterized.class) public class FirebaseAuthTest { - private static final String ACCESS_TOKEN = "mockaccesstoken"; - private static final String CLIENT_SECRET = "mockclientsecret"; - private static final String CLIENT_ID = "mockclientid"; - private static final String REFRESH_TOKEN = "mockrefreshtoken"; - private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); - - private final FirebaseOptions firebaseOptions; - private final boolean isCertCredential; - - public FirebaseAuthTest(FirebaseOptions baseOptions, boolean isCertCredential) { - this.firebaseOptions = baseOptions; - this.isCertCredential = isCertCredential; - } - - @Parameters - public static Collection data() throws Exception { - // Initialize this test suite with all available credential implementations. - return Arrays.asList( - new Object[][] { - { - new FirebaseOptions.Builder().setCredentials(createCertificateCredential()).build(), - /* isCertCredential */ true - }, - { - new FirebaseOptions.Builder() - .setCredentials(createRefreshTokenCredential()) - .setProjectId("test-project-id") - .build(), - /* isCertCredential */ false - }, - { - new FirebaseOptions.Builder() - .setCredentials(TestUtils.getApplicationDefaultCredentials()) - .build(), - /* isCertCredential */ false - }, - }); - } - - private static GoogleCredentials createRefreshTokenCredential() throws IOException { - - final MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addClient(CLIENT_ID, CLIENT_SECRET); - transport.addRefreshToken(REFRESH_TOKEN, ACCESS_TOKEN); - - Map secretJson = new HashMap<>(); - secretJson.put("client_id", CLIENT_ID); - secretJson.put("client_secret", CLIENT_SECRET); - secretJson.put("refresh_token", REFRESH_TOKEN); - secretJson.put("type", "authorized_user"); - InputStream refreshTokenStream = - new ByteArrayInputStream(JSON_FACTORY.toByteArray(secretJson)); - - return UserCredentials.fromStream(refreshTokenStream, new HttpTransportFactory() { - @Override - public HttpTransport create() { - return transport; - } - }); - } - - private static GoogleCredentials createCertificateCredential() throws IOException { - final MockTokenServerTransport transport = new MockTokenServerTransport( - "https://accounts.google.com/o/oauth2/token"); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - return ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream(), - new HttpTransportFactory() { - @Override - public HttpTransport create() { - return transport; - } - }); - } + private static final FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) + .build(); @Before public void setup() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseApp.initializeApp(firebaseOptions); } @@ -163,8 +65,6 @@ public void testGetInstance() { FirebaseAuth defaultAuth = FirebaseAuth.getInstance(); assertNotNull(defaultAuth); assertSame(defaultAuth, FirebaseAuth.getInstance()); - String token = TestOnlyImplFirebaseTrampolines.getToken(FirebaseApp.getInstance(), false); - Assert.assertTrue(!token.isEmpty()); } @Test @@ -173,8 +73,6 @@ public void testGetInstanceForApp() { FirebaseAuth auth = FirebaseAuth.getInstance(app); assertNotNull(auth); assertSame(auth, FirebaseAuth.getInstance(app)); - String token = TestOnlyImplFirebaseTrampolines.getToken(app, false); - Assert.assertTrue(!token.isEmpty()); } @Test @@ -234,126 +132,26 @@ public void testInitAfterAppDelete() throws ExecutionException, InterruptedExcep assertNotNull(auth2); assertNotSame(auth1, auth2); - if (isCertCredential) { - ApiFuture future = auth2.createCustomTokenAsync("foo"); - assertNotNull(future); - assertNotNull(future.get(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } - } - - @Test - public void testAppWithAuthVariableOverrides() { - Map authVariableOverrides = Collections.singletonMap("uid", (Object) "uid1"); - FirebaseOptions options = - new FirebaseOptions.Builder(firebaseOptions) - .setDatabaseAuthVariableOverride(authVariableOverrides) - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "testGetAppWithUid"); - assertEquals("uid1", app.getOptions().getDatabaseAuthVariableOverride().get("uid")); - String token = TestOnlyImplFirebaseTrampolines.getToken(app, false); - Assert.assertTrue(!token.isEmpty()); - } - - @Test - public void testCreateCustomToken() throws Exception { - GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions); - Assume.assumeTrue("Skipping testCredentialCertificateRequired for cert credential", - credentials instanceof ServiceAccountCredentials); - - FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testCreateCustomToken"); - FirebaseAuth auth = FirebaseAuth.getInstance(app); - - String token = auth.createCustomTokenAsync("user1").get(); - - FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); - assertEquals(parsedToken.getPayload().getUid(), "user1"); - assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail()); - assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail()); - assertNull(parsedToken.getPayload().getDeveloperClaims()); - assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken)); - } - - @Test - public void testCreateCustomTokenWithDeveloperClaims() throws Exception { - GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions); - Assume.assumeTrue("Skipping testCredentialCertificateRequired for cert credential", - credentials instanceof ServiceAccountCredentials); - - FirebaseApp app = - FirebaseApp.initializeApp(firebaseOptions, "testCreateCustomTokenWithDeveloperClaims"); - FirebaseAuth auth = FirebaseAuth.getInstance(app); - - String token = - auth.createCustomTokenAsync("user1", MapBuilder.of("claim", "value")).get(); - - FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); - assertEquals(parsedToken.getPayload().getUid(), "user1"); - assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail()); - assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail()); - assertEquals(parsedToken.getPayload().getDeveloperClaims().keySet().size(), 1); - assertEquals(parsedToken.getPayload().getDeveloperClaims().get("claim"), "value"); - assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken)); - } - - @Test - public void testServiceAccountRequired() throws Exception { - GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions); - Assume.assumeFalse("Skipping testServiceAccountRequired for service account credentials", - credentials instanceof ServiceAccountCredentials); - - FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testServiceAccountRequired"); - try { - FirebaseAuth.getInstance(app).createCustomTokenAsync("foo").get(); - fail("Expected exception."); - } catch (IllegalStateException expected) { - Assert.assertEquals( - "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK with " - + "service account credentials or specify a service account ID with " - + "iam.serviceAccounts.signBlob permission. Please refer to " - + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details " - + "on creating custom tokens.", - expected.getMessage()); - } + ApiFuture future = auth2.createCustomTokenAsync("foo"); + assertNotNull(future); + assertNotNull(future.get(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); } @Test - public void testProjectIdRequired() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testProjectIdRequired"); - String projectId = ImplFirebaseTrampolines.getProjectId(app); - Assume.assumeTrue("Skipping testProjectIdRequired for settings with project ID", - Strings.isNullOrEmpty(projectId)); - + public void testProjectIdRequired() { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials()) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options, "testProjectIdRequired"); try { - FirebaseAuth.getInstance(app).verifyIdTokenAsync("foo").get(); + FirebaseAuth.getInstance(app); fail("Expected exception."); } catch (IllegalArgumentException expected) { Assert.assertEquals( - "Must initialize FirebaseApp with a project ID to call verifyIdToken()", - expected.getMessage()); - } - } - - @Test - public void testVerifyIdTokenWithExplicitProjectId() throws Exception { - GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions); - Assume.assumeFalse( - "Skipping testVerifyIdTokenWithExplicitProjectId for service account credentials", - credentials instanceof ServiceAccountCredentials); - - FirebaseOptions options = - new FirebaseOptions.Builder(firebaseOptions) - .setProjectId("mock-project-id") - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "testVerifyIdTokenWithExplicitProjectId"); - try { - FirebaseAuth.getInstance(app).verifyIdTokenAsync("foo").get(); - fail("Expected exception."); - } catch (ExecutionException expected) { - Assert.assertNotEquals( - "com.google.firebase.FirebaseException: Must initialize FirebaseApp with a project ID " - + "to call verifyIdToken()", + "Project ID is required to access the auth service. Use a service account credential " + + "or set the project ID explicitly via FirebaseOptions. Alternatively you can " + + "also set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.", expected.getMessage()); - assertTrue(expected.getCause() instanceof IllegalArgumentException); } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java new file mode 100644 index 000000000..17a99c3f3 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.internal.FirebaseCustomAuthToken; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.ServiceAccount; +import java.util.List; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +public class FirebaseCustomTokenTest { + + @After + public void cleanup() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCreateCustomToken() throws Exception { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream())) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + FirebaseAuth auth = FirebaseAuth.getInstance(app); + + String token = auth.createCustomTokenAsync("user1").get(); + FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); + assertEquals(parsedToken.getPayload().getUid(), "user1"); + assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail()); + assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail()); + assertNull(parsedToken.getPayload().getDeveloperClaims()); + assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken)); + } + + @Test + public void testCreateCustomTokenWithDeveloperClaims() throws Exception { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream())) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + FirebaseAuth auth = FirebaseAuth.getInstance(app); + + String token = auth.createCustomTokenAsync( + "user1", MapBuilder.of("claim", "value")).get(); + FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); + assertEquals(parsedToken.getPayload().getUid(), "user1"); + assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail()); + assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail()); + assertEquals(parsedToken.getPayload().getDeveloperClaims().keySet().size(), 1); + assertEquals(parsedToken.getPayload().getDeveloperClaims().get("claim"), "value"); + assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken)); + } + + @Test + public void testCreateCustomTokenWithoutServiceAccountCredentials() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + String content = Utils.getDefaultJsonFactory().toString( + ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes()))); + response.setContent(content); + MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of(response)); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project-id") + .setServiceAccountId("test@service.account") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + FirebaseAuth auth = FirebaseAuth.getInstance(app); + + String token = auth.createCustomTokenAsync("user1").get(); + FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); + assertEquals(parsedToken.getPayload().getUid(), "user1"); + assertEquals(parsedToken.getPayload().getSubject(), "test@service.account"); + assertEquals(parsedToken.getPayload().getIssuer(), "test@service.account"); + assertNull(parsedToken.getPayload().getDeveloperClaims()); + assertEquals("test-signature", new String(parsedToken.getSignatureBytes())); + } + + @Test + public void testCreateCustomTokenWithDiscoveredServiceAccount() throws Exception { + String content = Utils.getDefaultJsonFactory().toString( + ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes()))); + List responses = ImmutableList.of( + // Service account discovery response + new MockLowLevelHttpResponse().setContent("test@service.account"), + + // Sign blob response + new MockLowLevelHttpResponse().setContent(content) + ); + MockHttpTransport transport = new MultiRequestMockHttpTransport(responses); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project-id") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + FirebaseAuth auth = FirebaseAuth.getInstance(app); + + String token = auth.createCustomTokenAsync("user1").get(); + FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); + assertEquals(parsedToken.getPayload().getUid(), "user1"); + assertEquals(parsedToken.getPayload().getSubject(), "test@service.account"); + assertEquals(parsedToken.getPayload().getIssuer(), "test@service.account"); + assertNull(parsedToken.getPayload().getDeveloperClaims()); + assertEquals("test-signature", new String(parsedToken.getSignatureBytes())); + } + + @Test + public void testNoServiceAccount() throws Exception { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project-id") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + try { + FirebaseAuth.getInstance(app).createCustomTokenAsync("foo").get(); + fail("Expected exception."); + } catch (IllegalStateException expected) { + Assert.assertEquals( + "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK with " + + "service account credentials or specify a service account ID with " + + "iam.serviceAccounts.signBlob permission. Please refer to " + + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details " + + "on creating custom tokens.", + expected.getMessage()); + } + } +} From b80bc9206481a1750ffcddae9c4dd6486d249543 Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Thu, 29 Nov 2018 13:08:05 -0800 Subject: [PATCH 025/441] Add android channel id support (#224) * Add android channel id support * Add android channel id support --- CHANGELOG.md | 3 +++ .../messaging/AndroidNotification.java | 19 +++++++++++++++++++ .../messaging/FirebaseMessagingTest.java | 2 ++ .../firebase/messaging/MessageTest.java | 2 ++ 4 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8bd41ff1..3f94cc430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ that are helpful when debugging problems. - [changed] Migrated the `FirebaseAuth` user management API to the new Identity Toolkit endpoint. +- [added] Added new `setChannelId()` to the + AndroidNotification.Builder API for setting the Android + notification channel ID (new in Android O). # v6.5.0 diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 27f315591..0a6cd0620 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -63,6 +63,9 @@ public class AndroidNotification { @Key("title_loc_args") private final List titleLocArgs; + + @Key("channel_id") + private final String channelId; private AndroidNotification(Builder builder) { this.title = builder.title; @@ -93,6 +96,7 @@ private AndroidNotification(Builder builder) { } else { this.titleLocArgs = null; } + this.channelId = builder.channelId; } /** @@ -117,6 +121,7 @@ public static class Builder { private List bodyLocArgs = new ArrayList<>(); private String titleLocKey; private List titleLocArgs = new ArrayList<>(); + private String channelId; private Builder() {} @@ -273,6 +278,20 @@ public Builder addAllTitleLocalizationArgs(@NonNull List args) { return this; } + /** + * Sets the Android notification channel ID (new in Android O). The app must create a channel + * with this channel ID before any notification with this channel ID is received. If you + * don't send this channel ID in the request, or if the channel ID provided has not yet been + * created by the app, FCM uses the channel ID specified in the app manifest. + * + * @param channelId The notification's channel ID. + * @return This builder. + */ + public Builder setChannelId(String channelId) { + this.channelId = channelId; + return this; + } + /** * Creates a new {@link AndroidNotification} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 1ad74ac33..d18e2be74 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -606,6 +606,7 @@ private static Map> buildTestMessages() { .addAllTitleLocalizationArgs(ImmutableList.of("t-arg2", "t-arg3")) .addBodyLocalizationArg("b-arg1") .addAllBodyLocalizationArgs(ImmutableList.of("b-arg2", "b-arg3")) + .setChannelId("channel-id") .build()) .build()) .setTopic("test-topic") @@ -629,6 +630,7 @@ private static Map> buildTestMessages() { .put("title_loc_args", ImmutableList.of("t-arg1", "t-arg2", "t-arg3")) .put("body_loc_key", "test-body-key") .put("body_loc_args", ImmutableList.of("b-arg1", "b-arg2", "b-arg3")) + .put("channel_id", "channel-id") .build() ) )); diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index b6025c0bb..a44cd2a81 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -147,6 +147,7 @@ public void testAndroidMessageWithNotification() throws IOException { .setBodyLocalizationKey("body-loc") .addBodyLocalizationArg("body-arg1") .addAllBodyLocalizationArgs(ImmutableList.of("body-arg2", "body-arg3")) + .setChannelId("channel-id") .build()) .build()) .setTopic("test-topic") @@ -163,6 +164,7 @@ public void testAndroidMessageWithNotification() throws IOException { .put("title_loc_args", ImmutableList.of("title-arg1", "title-arg2", "title-arg3")) .put("body_loc_key", "body-loc") .put("body_loc_args", ImmutableList.of("body-arg1", "body-arg2", "body-arg3")) + .put("channel_id", "channel-id") .build(); Map data = ImmutableMap.of( "collapse_key", "test-key", From 89276407b469d38b6db0a663bf85afd5f61cd766 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 29 Nov 2018 17:28:17 -0800 Subject: [PATCH 026/441] Closing Firestore instance on FirebaseApp.delete() (#227) * Closing Firestore instance on FirebaseApp.delete() * Updated javadocs * Updated docs and tests --- CHANGELOG.md | 3 +- .../firebase/cloud/FirestoreClient.java | 21 ++++++++--- .../firebase/cloud/FirestoreClientTest.java | 37 ++++++++++++------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f94cc430..f2767d13c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [fixed] `Firestore` instances initialized by the SDK are now cleaned + up, when `FirebaseApp.delete()` is called. # v6.6.0 diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index 15171d14b..fddf3320d 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -11,6 +11,8 @@ import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * {@code FirestoreClient} provides access to Google Cloud Firestore. Use this API to obtain a @@ -26,6 +28,8 @@ */ public class FirestoreClient { + private static final Logger logger = LoggerFactory.getLogger(FirestoreClient.class); + private final Firestore firestore; private FirestoreClient(FirebaseApp app) { @@ -48,7 +52,9 @@ private FirestoreClient(FirebaseApp app) { } /** - * Returns the Firestore instance associated with the default Firebase app. + * Returns the Firestore instance associated with the default Firebase app. Returns the same + * instance for all invocations. The Firestore instance and all references obtained from it + * becomes unusable, once the default app is deleted. * * @return A non-null {@code Firestore} * instance. @@ -59,7 +65,9 @@ public static Firestore getFirestore() { } /** - * Returns the Firestore instance associated with the specified Firebase app. + * Returns the Firestore instance associated with the specified Firebase app. For a given app, + * always returns the same instance. The Firestore instance and all references obtained from it + * becomes unusable, once the specified app is deleted. * * @param app A non-null {@link FirebaseApp}. * @return A non-null {@code Firestore} @@ -89,10 +97,11 @@ private static class FirestoreClientService extends FirebaseService Date: Fri, 30 Nov 2018 13:49:13 -0800 Subject: [PATCH 027/441] More unit tests for project management API (#229) * More unit tests for project management API * Fixing minor formatting issues --- .../projectmanagement/AndroidAppTest.java | 18 ++++++++++++ ...ebaseProjectManagementServiceImplTest.java | 29 +++++++++++++++++++ .../projectmanagement/IosAppTest.java | 22 ++++++++++++++ .../projectmanagement/ShaCertificateTest.java | 19 ++++++++++++ 4 files changed, 88 insertions(+) diff --git a/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java b/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java index 3f6529c7e..892fd3dcb 100644 --- a/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/AndroidAppTest.java @@ -189,6 +189,24 @@ public void testDeleteShaCertificateAsync() throws Exception { Mockito.verify(androidAppService).deleteShaCertificateAsync(CERTIFICATE_NAME); } + @Test + public void testAndroidAppMetadata() { + assertEquals(APP_NAME, ANDROID_APP_METADATA.getName()); + assertEquals(APP_ID, ANDROID_APP_METADATA.getAppId()); + assertEquals(APP_DISPLAY_NAME, ANDROID_APP_METADATA.getDisplayName()); + assertEquals(PROJECT_ID, ANDROID_APP_METADATA.getProjectId()); + assertEquals(APP_PACKAGE_NAME, ANDROID_APP_METADATA.getPackageName()); + } + + @Test + public void testAndroidAppMetadataEquality() { + AndroidAppMetadata other = + new AndroidAppMetadata(APP_NAME, APP_ID, APP_DISPLAY_NAME, PROJECT_ID, APP_PACKAGE_NAME); + + assertEquals(ANDROID_APP_METADATA.hashCode(), other.hashCode()); + assertEquals(ANDROID_APP_METADATA, other); + } + private SettableApiFuture createApiFuture(T value) { final SettableApiFuture future = SettableApiFuture.create(); future.set(value); diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index 45b51a227..edf620ca5 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.HttpRequest; @@ -258,6 +259,20 @@ public void getIosAppAsync() throws Exception { assertEquals(iosAppMetadata, IOS_APP_METADATA); } + @Test + public void getIosAppHttpError() { + firstRpcResponse.setStatusCode(500); + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals("App ID \"test-ios-app-id\": Internal server error.", e.getMessage()); + } + } + @Test public void getIosAppNoDisplayName() throws Exception { String expectedUrl = String.format( @@ -526,6 +541,20 @@ public void getAndroidAppAsync() throws Exception { assertEquals(androidAppMetadata, ANDROID_APP_METADATA); } + @Test + public void getAndroidAppHttpError() { + firstRpcResponse.setStatusCode(500); + firstRpcResponse.setContent("{}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals("App ID \"test-android-app-id\": Internal server error.", e.getMessage()); + } + } + @Test public void getAndroidAppNoDisplayName() throws Exception { firstRpcResponse.setContent(GET_ANDROID_NO_DISPLAY_NAME_RESPONSE); diff --git a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java index fbc4fc8ad..269d3dae0 100644 --- a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java @@ -199,6 +199,28 @@ public void testGetConfigAsyncShouldRethrow() throws Exception { } } + @Test + public void testIosAppMetadata() { + assertEquals(TEST_APP_NAME, TEST_IOS_APP_METADATA.getName()); + assertEquals(TEST_APP_ID, TEST_IOS_APP_METADATA.getAppId()); + assertEquals(TEST_APP_DISPLAY_NAME, TEST_IOS_APP_METADATA.getDisplayName()); + assertEquals(TEST_PROJECT_ID, TEST_IOS_APP_METADATA.getProjectId()); + assertEquals(TEST_APP_BUNDLE_ID, TEST_IOS_APP_METADATA.getBundleId()); + } + + @Test + public void testIosAppMetadataEquality() { + IosAppMetadata other = new IosAppMetadata( + TEST_APP_NAME, + TEST_APP_ID, + TEST_APP_DISPLAY_NAME, + TEST_PROJECT_ID, + TEST_APP_BUNDLE_ID); + + assertEquals(TEST_IOS_APP_METADATA.hashCode(), other.hashCode()); + assertEquals(TEST_IOS_APP_METADATA, other); + } + private ApiFuture immediateIosAppMetadataFailedFuture() { return ApiFutures.immediateFailedFuture(FIREBASE_PROJECT_MANAGEMENT_EXCEPTION); } diff --git a/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java b/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java index f5ef0911e..1b7c9c97b 100644 --- a/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/ShaCertificateTest.java @@ -58,4 +58,23 @@ public void getTypeFromHashSha256WithIncorrectSize() { ShaCertificate.getTypeFromHash( "1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"); } + + @Test + public void testEquality() { + ShaCertificate shaOne = ShaCertificate.create("1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"); + ShaCertificate shaTwo = ShaCertificate.create("1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"); + + assertEquals(shaOne, shaTwo); + assertEquals(shaOne.hashCode(), shaTwo.hashCode()); + } + + @Test + public void testToString() { + ShaCertificate sha = ShaCertificate.create("cert", "1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA"); + + assertEquals( + "ShaCertificate{name=cert, shaHash=1111AAAA1111AAAA1111AAAA1111AAAA1111AAAA, " + + "certType=SHA_1}", + sha.toString()); + } } From a38e47ca171eb6a40a54b7f9e2f9e49b0c02aa93 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 Dec 2018 11:24:27 -0800 Subject: [PATCH 028/441] Implemented CriticalSound API for FCM (#233) * Implemented CriticalSounf API for FCM * Made the name field required * Ignoring empty values in sound field * Documentation updates --- CHANGELOG.md | 2 + .../com/google/firebase/messaging/Aps.java | 21 +++- .../firebase/messaging/CriticalSound.java | 115 ++++++++++++++++++ .../firebase/messaging/MessageTest.java | 95 ++++++++++++--- 4 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/CriticalSound.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f2767d13c..d59a9fbb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- `Aps` class now supports configuring a critical sound. A new + `CriticalSound` class has been introduced for this purpose. - [fixed] `Firestore` instances initialized by the SDK are now cleaned up, when `FirebaseApp.delete()` is called. diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java index 17912d1e3..10c5af181 100644 --- a/src/main/java/com/google/firebase/messaging/Aps.java +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -35,6 +35,8 @@ public class Aps { private Aps(Builder builder) { checkArgument(Strings.isNullOrEmpty(builder.alertString) || (builder.alert == null), "Multiple alert specifications (string and ApsAlert) found."); + checkArgument(Strings.isNullOrEmpty(builder.sound) || (builder.criticalSound == null), + "Multiple sound specifications (sound and CriticalSound) found."); ImmutableMap.Builder fields = ImmutableMap.builder(); if (builder.alert != null) { fields.put("alert", builder.alert); @@ -44,8 +46,10 @@ private Aps(Builder builder) { if (builder.badge != null) { fields.put("badge", builder.badge); } - if (builder.sound != null) { + if (!Strings.isNullOrEmpty(builder.sound)) { fields.put("sound", builder.sound); + } else if (builder.criticalSound != null) { + fields.put("sound", builder.criticalSound.getFields()); } if (builder.contentAvailable) { fields.put("content-available", 1); @@ -82,6 +86,7 @@ public static class Builder { private ApsAlert alert; private Integer badge; private String sound; + private CriticalSound criticalSound; private boolean contentAvailable; private boolean mutableContent; private String category; @@ -125,7 +130,8 @@ public Builder setBadge(int badge) { } /** - * Sets the sound to be played with the message. + * Sets the sound to be played with the message. For critical alerts use the + * {@link #setSound(CriticalSound)} method. * * @param sound Sound file name or {@code "default"}. * @return This builder. @@ -135,6 +141,17 @@ public Builder setSound(String sound) { return this; } + /** + * Sets the critical alert sound to be played with the message. + * + * @param sound A {@link CriticalSound} instance containing the alert sound configuration. + * @return This builder. + */ + public Builder setSound(CriticalSound sound) { + this.criticalSound = sound; + return this; + } + /** * Specifies whether to configure a background update notification. * diff --git a/src/main/java/com/google/firebase/messaging/CriticalSound.java b/src/main/java/com/google/firebase/messaging/CriticalSound.java new file mode 100644 index 000000000..ed293ba96 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/CriticalSound.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.messaging; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * The sound configuration for APNs critical alerts. + */ +public final class CriticalSound { + + private final Map fields; + + private CriticalSound(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.name), "name must not be null or empty"); + ImmutableMap.Builder fields = ImmutableMap.builder() + .put("name", builder.name); + if (builder.critical) { + fields.put("critical", 1); + } + if (builder.volume != null) { + checkArgument(builder.volume >= 0 && builder.volume <= 1, + "volume must be in the interval [0,1]"); + fields.put("volume", builder.volume); + } + this.fields = fields.build(); + } + + Map getFields() { + return fields; + } + + /** + * Creates a new {@link CriticalSound.Builder}. + * + * @return A {@link CriticalSound.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private boolean critical; + private String name; + private Double volume; + + private Builder() { + } + + /** + * Sets the critical alert flag on the sound configuration. + * + * @param critical True to set the critical alert flag. + * @return This builder. + */ + public Builder setCritical(boolean critical) { + this.critical = critical; + return this; + } + + /** + * The name of a sound file in your app's main bundle or in the {@code Library/Sounds} folder + * of your app’s container directory. Specify the string {@code default} to play the system + * sound. + * + * @param name Sound file name. + * @return This builder. + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + /** + * The volume for the critical alert's sound. Must be a value between 0.0 (silent) and 1.0 + * (full volume). + * + * @param volume A volume between 0.0 (inclusive) and 1.0 (inclusive). + * @return This builder. + */ + public Builder setVolume(double volume) { + this.volume = volume; + return this; + } + + /** + * Builds a new {@link CriticalSound} instance from the fields set on this builder. + * + * @return A non-null {@link CriticalSound}. + * @throws IllegalArgumentException If the volume value is out of range. + */ + public CriticalSound build() { + return new CriticalSound(this); + } + } +} diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index a44cd2a81..5b53b1a4b 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -448,6 +448,72 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { message); } + @Test + public void testApnsMessageWithCriticalSound() throws IOException { + Message message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder() + .setSound(CriticalSound.builder() // All fields + .setCritical(true) + .setName("default") + .setVolume(0.5) + .build()) + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map payload = ImmutableMap.of( + "aps", ImmutableMap.builder() + .put("sound", ImmutableMap.of( + "critical", new BigDecimal(1), + "name", "default", + "volume", new BigDecimal(0.5))) + .build()); + assertJsonEquals( + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of("payload", payload)), + message); + + message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder() + .setSound(CriticalSound.builder() // Name field only + .setName("default") + .build()) + .build()) + .build()) + .setTopic("test-topic") + .build(); + payload = ImmutableMap.of( + "aps", ImmutableMap.builder() + .put("sound", ImmutableMap.of("name", "default")) + .build()); + assertJsonEquals( + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of("payload", payload)), + message); + } + + @Test + public void testInvalidCriticalSound() { + List soundBuilders = ImmutableList.of( + CriticalSound.builder(), + CriticalSound.builder().setCritical(true).setVolume(0.5), + CriticalSound.builder().setVolume(-0.1), + CriticalSound.builder().setVolume(1.1) + ); + for (int i = 0; i < soundBuilders.size(); i++) { + try { + soundBuilders.get(i).build(); + fail("No error thrown for invalid sound: " + i); + } catch (IllegalArgumentException expected) { + // expected + } + } + } + @Test public void testInvalidApnsConfig() { List configBuilders = ImmutableList.of( @@ -464,23 +530,22 @@ public void testInvalidApnsConfig() { } } - Aps.Builder builder = Aps.builder().setAlert("string").setAlert(ApsAlert.builder().build()); - try { - builder.build(); - fail("No error thrown for invalid aps"); - } catch (IllegalArgumentException expected) { - // expected - } - - builder = Aps.builder().setMutableContent(true).putCustomData("mutable-content", 1); - try { - builder.build(); - fail("No error thrown for invalid aps"); - } catch (IllegalArgumentException expected) { - // expected + List apsBuilders = ImmutableList.of( + Aps.builder().setAlert("string").setAlert(ApsAlert.builder().build()), + Aps.builder().setSound("default").setSound(CriticalSound.builder() + .setName("default") + .build()), + Aps.builder().setMutableContent(true).putCustomData("mutable-content", 1) + ); + for (int i = 0; i < apsBuilders.size(); i++) { + try { + apsBuilders.get(i).build(); + fail("No error thrown for invalid aps: " + i); + } catch (IllegalArgumentException expected) { + // expected + } } - List notificationBuilders = ImmutableList.of( ApsAlert.builder().addLocalizationArg("foo"), ApsAlert.builder().addTitleLocalizationArg("foo"), From 5238381d4cfabc1586eb980dec500197356f8fb8 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 19 Dec 2018 13:35:30 -0800 Subject: [PATCH 029/441] FirebaseAuth Email Action Links API (#232) * Added ActionCodeSettings API * Added full ActionCodeSettings API * Migrating Auth API to the new Identity Toolkit endpoint * Updated CHANGELOG * Implemented the public API surface * More tests and documentation * Added more documentation and tests * Added integration tests; Fixed how action code settings are serialized * Removed [Android|Ios]ActionCodeSettings * Adding newline at eof * Update CHANGELOG.md --- CHANGELOG.md | 9 +- .../firebase/auth/ActionCodeSettings.java | 199 +++++++++++++++ .../google/firebase/auth/FirebaseAuth.java | 176 +++++++++++++ .../firebase/auth/FirebaseUserManager.java | 27 ++ .../firebase/auth/ActionCodeSettingsTest.java | 87 +++++++ .../google/firebase/auth/FirebaseAuthIT.java | 231 ++++++++++++++---- .../auth/FirebaseUserManagerTest.java | 219 +++++++++++++++++ .../testing/IntegrationTestUtils.java | 1 + src/test/resources/generateEmailLink.json | 4 + 9 files changed, 903 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/ActionCodeSettings.java create mode 100644 src/test/java/com/google/firebase/auth/ActionCodeSettingsTest.java create mode 100644 src/test/resources/generateEmailLink.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d59a9fbb9..0f9deac51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # Unreleased +- [added] Added `generatePasswordResetLink()`, `generateEmailVerificationLink()` + and `generateSignInWithEmailLink()` methods to the `FirebaseAuth` API. - `Aps` class now supports configuring a critical sound. A new `CriticalSound` class has been introduced for this purpose. - [fixed] `Firestore` instances initialized by the SDK are now cleaned up, when `FirebaseApp.delete()` is called. +- [added] Added new `setChannelId()` method to the + `AndroidNotification.Builder` API for setting the Android + notification channel ID (new in Android O). # v6.6.0 @@ -15,10 +20,6 @@ - [fixed] FCM errors sent by the back-end now include more details that are helpful when debugging problems. - [changed] Migrated the `FirebaseAuth` user management API to the - new Identity Toolkit endpoint. -- [added] Added new `setChannelId()` to the - AndroidNotification.Builder API for setting the Android - notification channel ID (new in Android O). # v6.5.0 diff --git a/src/main/java/com/google/firebase/auth/ActionCodeSettings.java b/src/main/java/com/google/firebase/auth/ActionCodeSettings.java new file mode 100644 index 000000000..67a196edf --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ActionCodeSettings.java @@ -0,0 +1,199 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import com.google.firebase.internal.NonNull; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +/** + * Defines the required continue/state URL with optional Android and iOS settings. Used when + * invoking the email action link generation APIs in {@link FirebaseAuth}. + */ +public final class ActionCodeSettings { + + private final Map properties; + + private ActionCodeSettings(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.url), "URL must not be null or empty"); + try { + new URL(builder.url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Malformed URL string", e); + } + if (builder.androidInstallApp || !Strings.isNullOrEmpty(builder.androidMinimumVersion)) { + checkArgument(!Strings.isNullOrEmpty(builder.androidPackageName), + "Android package name is required when specifying other Android settings"); + } + ImmutableMap.Builder properties = ImmutableMap.builder() + .put("continueUrl", builder.url) + .put("canHandleCodeInApp", builder.handleCodeInApp); + if (!Strings.isNullOrEmpty(builder.dynamicLinkDomain)) { + properties.put("dynamicLinkDomain", builder.dynamicLinkDomain); + } + if (!Strings.isNullOrEmpty(builder.iosBundleId)) { + properties.put("iOSBundleId", builder.iosBundleId); + } + if (!Strings.isNullOrEmpty(builder.androidPackageName)) { + properties.put("androidPackageName", builder.androidPackageName); + if (!Strings.isNullOrEmpty(builder.androidMinimumVersion)) { + properties.put("androidMinimumVersion", builder.androidMinimumVersion); + } + if (builder.androidInstallApp) { + properties.put("androidInstallApp", builder.androidInstallApp); + } + } + this.properties = properties.build(); + } + + Map getProperties() { + return this.properties; + } + + /** + * Creates a new {@link ActionCodeSettings.Builder}. + * + * @return A {@link ActionCodeSettings.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private String url; + private boolean handleCodeInApp; + private String dynamicLinkDomain; + private String iosBundleId; + private String androidPackageName; + private String androidMinimumVersion; + private boolean androidInstallApp; + + private Builder() { } + + /** + * Sets the link continue/state URL, which has different meanings in different contexts: + * + *

+ * + *

This parameter must be specified when creating a new {@link ActionCodeSettings} instance. + * + * @param url Continue/state URL string. + * @return This builder. + */ + public Builder setUrl(@NonNull String url) { + this.url = url; + return this; + } + + /** + * Specifies whether to open the link via a mobile app or a browser. The default is false. + * When set to true, the action code link is sent as a Universal Link or an Android App Link + * and is opened by the app if installed. In the false case, the code is sent to the web widget + * first and then redirects to the app if installed. + * + * @param handleCodeInApp true to open the link in the app, and false otherwise. + * @return This builder. + */ + public Builder setHandleCodeInApp(boolean handleCodeInApp) { + this.handleCodeInApp = handleCodeInApp; + return this; + } + + /** + * Sets the dynamic link domain to use for the current link if it is to be opened using + * Firebase Dynamic Links, as multiple dynamic link domains can be configured per project. This + * setting provides the ability to explicitly choose one. If none is provided, the oldest + * domain is used by default. + * + * @param dynamicLinkDomain Firebase Dynamic Link domain string. + * @return This builder. + */ + public Builder setDynamicLinkDomain(String dynamicLinkDomain) { + this.dynamicLinkDomain = dynamicLinkDomain; + return this; + } + + /** + * Sets the bundle ID of the iOS app where the link should be handled if the + * application is already installed on the device. + * + * @param iosBundleId The iOS bundle ID string. + */ + public Builder setIosBundleId(String iosBundleId) { + this.iosBundleId = iosBundleId; + return this; + } + + /** + * Sets the Android package name of the app where the link should be handled if the + * Android app is installed. Must be specified when setting other Android-specific settings. + * + * @param androidPackageName Package name string. Must be specified, and must not be null + * or empty. + * @return This builder. + */ + public Builder setAndroidPackageName(String androidPackageName) { + this.androidPackageName = androidPackageName; + return this; + } + + /** + * Sets the minimum version for Android app. If the installed app is an older version, the user + * is taken to the Play Store to upgrade the app. + * + * @param androidMinimumVersion Minimum version string. + * @return This builder. + */ + public Builder setAndroidMinimumVersion(String androidMinimumVersion) { + this.androidMinimumVersion = androidMinimumVersion; + return this; + } + + /** + * Specifies whether to install the Android app if the device supports it and the app is not + * already installed. + * + * @param androidInstallApp true to install the app, and false otherwise. + * @return This builder. + */ + public Builder setAndroidInstallApp(boolean androidInstallApp) { + this.androidInstallApp = androidInstallApp; + return this; + } + + /** + * Builds a new {@link ActionCodeSettings}. + * + * @return A non-null {@link ActionCodeSettings}. + */ + public ActionCodeSettings build() { + return new ActionCodeSettings(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index eb258952a..ec1d622f5 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -27,6 +27,7 @@ import com.google.common.base.Strings; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; import com.google.firebase.auth.ListUsersPage.DefaultUserSource; import com.google.firebase.auth.ListUsersPage.PageFactory; @@ -969,6 +970,181 @@ protected UserImportResult execute() throws FirebaseAuthException { }; } + /** + * Generates the out-of-band email action link for password reset flows for the specified email + * address. + * + * @param email The email of the user whose password is to be reset. + * @return A password reset link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generatePasswordResetLink(@NonNull String email) throws FirebaseAuthException { + return generatePasswordResetLink(email, null); + } + + /** + * Generates the out-of-band email action link for password reset flows for the specified email + * address. + * + * @param email The email of the user whose password is to be reset. + * @param settings The action code settings which defines whether + * the link is to be handled by a mobile app and the additional state information to be + * passed in the deep link, etc. + * @return A password reset link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generatePasswordResetLink( + @NonNull String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { + return generateEmailActionLinkOp(EmailLinkType.PASSWORD_RESET, email, settings).call(); + } + + /** + * Similar to {@link #generatePasswordResetLink(String)} but performs the operation + * asynchronously. + * + * @param email The email of the user whose password is to be reset. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generatePasswordResetLinkAsync(@NonNull String email) { + return generatePasswordResetLinkAsync(email, null); + } + + /** + * Similar to {@link #generatePasswordResetLink(String, ActionCodeSettings)} but performs the + * operation asynchronously. + * + * @param email The email of the user whose password is to be reset. + * @param settings The action code settings which defines whether + * the link is to be handled by a mobile app and the additional state information to be + * passed in the deep link, etc. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generatePasswordResetLinkAsync( + @NonNull String email, @Nullable ActionCodeSettings settings) { + return generateEmailActionLinkOp(EmailLinkType.PASSWORD_RESET, email, settings) + .callAsync(firebaseApp); + } + + /** + * Generates the out-of-band email action link for email verification flows for the specified + * email address. + * + * @param email The email of the user to be verified. + * @return An email verification link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateEmailVerificationLink(@NonNull String email) throws FirebaseAuthException { + return generateEmailVerificationLink(email, null); + } + + /** + * Generates the out-of-band email action link for email verification flows for the specified + * email address, using the action code settings provided. + * + * @param email The email of the user to be verified. + * @return An email verification link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateEmailVerificationLink( + @NonNull String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { + return generateEmailActionLinkOp(EmailLinkType.VERIFY_EMAIL, email, settings).call(); + } + + /** + * Similar to {@link #generateEmailVerificationLink(String)} but performs the + * operation asynchronously. + * + * @param email The email of the user to be verified. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generateEmailVerificationLinkAsync(@NonNull String email) { + return generateEmailVerificationLinkAsync(email, null); + } + + /** + * Similar to {@link #generateEmailVerificationLink(String, ActionCodeSettings)} but performs the + * operation asynchronously. + * + * @param email The email of the user to be verified. + * @param settings The action code settings which defines whether + * the link is to be handled by a mobile app and the additional state information to be + * passed in the deep link, etc. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generateEmailVerificationLinkAsync( + @NonNull String email, @Nullable ActionCodeSettings settings) { + return generateEmailActionLinkOp(EmailLinkType.VERIFY_EMAIL, email, settings) + .callAsync(firebaseApp); + } + + /** + * Generates the out-of-band email action link for email link sign-in flows, using the action + * code settings provided. + * + * @param email The email of the user signing in. + * @param settings The action code settings which defines whether + * the link is to be handled by a mobile app and the additional state information to be + * passed in the deep link, etc. + * @return An email verification link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateSignInWithEmailLink( + @NonNull String email, @NonNull ActionCodeSettings settings) throws FirebaseAuthException { + return generateEmailActionLinkOp(EmailLinkType.EMAIL_SIGNIN, email, settings).call(); + } + + /** + * Similar to {@link #generateSignInWithEmailLink(String, ActionCodeSettings)} but performs the + * operation asynchronously. + * + * @param email The email of the user signing in. + * @param settings The action code settings which defines whether + * the link is to be handled by a mobile app and the additional state information to be + * passed in the deep link, etc. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws NullPointerException If the settings is null. + */ + public ApiFuture generateSignInWithEmailLinkAsync( + String email, @NonNull ActionCodeSettings settings) { + return generateEmailActionLinkOp(EmailLinkType.EMAIL_SIGNIN, email, settings) + .callAsync(firebaseApp); + } + + private CallableOperation generateEmailActionLinkOp( + final EmailLinkType type, final String email, final ActionCodeSettings settings) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + if (type == EmailLinkType.EMAIL_SIGNIN) { + checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links"); + } + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.getEmailActionLink(type, email, settings); + } + }; + } + @VisibleForTesting FirebaseUserManager getUserManager() { return this.userManager; diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 294f10a0f..7f534fda2 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -85,6 +85,8 @@ class FirebaseUserManager { .put("PROJECT_NOT_FOUND", "project-not-found") .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR) .put("WEAK_PASSWORD", "invalid-password") + .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri") + .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") .build(); static final int MAX_LIST_USERS_RESULTS = 1000; @@ -235,6 +237,25 @@ String createSessionCookie(String idToken, throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie"); } + String getEmailActionLink(EmailLinkType type, String email, + @Nullable ActionCodeSettings settings) throws FirebaseAuthException { + ImmutableMap.Builder payload = ImmutableMap.builder() + .put("requestType", type.name()) + .put("email", email) + .put("returnOobLink", true); + if (settings != null) { + payload.putAll(settings.getProperties()); + } + GenericJson response = post("/accounts:sendOobCode", payload.build(), GenericJson.class); + if (response != null) { + String link = (String) response.get("oobLink"); + if (!Strings.isNullOrEmpty(link)) { + return link; + } + } + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link"); + } + private T post(String path, Object content, Class clazz) throws FirebaseAuthException { checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty"); checkNotNull(content, "content must not be null for POST requests"); @@ -325,4 +346,10 @@ int getUsersCount() { return users.size(); } } + + enum EmailLinkType { + VERIFY_EMAIL, + EMAIL_SIGNIN, + PASSWORD_RESET, + } } diff --git a/src/test/java/com/google/firebase/auth/ActionCodeSettingsTest.java b/src/test/java/com/google/firebase/auth/ActionCodeSettingsTest.java new file mode 100644 index 000000000..501aa53ba --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ActionCodeSettingsTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.Test; + +public class ActionCodeSettingsTest { + + @Test(expected = IllegalArgumentException.class) + public void testNoUrl() { + ActionCodeSettings.builder().build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testMalformedUrl() { + ActionCodeSettings.builder() + .setUrl("not a url") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyUrl() { + ActionCodeSettings.builder() + .setUrl("") + .build(); + } + + @Test + public void testUrlOnly() { + ActionCodeSettings settings = ActionCodeSettings.builder() + .setUrl("https://example.com") + .build(); + Map expected = ImmutableMap.of( + "continueUrl", "https://example.com", "canHandleCodeInApp", false); + assertEquals(expected, settings.getProperties()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoAndroidPackageName() { + ActionCodeSettings.builder() + .setUrl("https://example.com") + .setAndroidMinimumVersion("6.0") + .setAndroidInstallApp(true) + .build(); + } + + @Test + public void testAllSettings() { + ActionCodeSettings settings = ActionCodeSettings.builder() + .setUrl("https://example.com") + .setHandleCodeInApp(true) + .setDynamicLinkDomain("myapp.page.link") + .setIosBundleId("com.example.ios") + .setAndroidPackageName("com.example.android") + .setAndroidMinimumVersion("6.0") + .setAndroidInstallApp(true) + .build(); + Map expected = ImmutableMap.builder() + .put("continueUrl", "https://example.com") + .put("canHandleCodeInApp", true) + .put("dynamicLinkDomain", "myapp.page.link") + .put("iOSBundleId", "com.example.ios") + .put("androidPackageName", "com.example.android") + .put("androidMinimumVersion", "6.0") + .put("androidInstallApp", true) + .build(); + assertEquals(expected, settings.getProperties()); + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 54c225960..b01eeb9b9 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -42,6 +42,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -50,7 +51,9 @@ import com.google.firebase.auth.hash.Scrypt; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; +import java.net.URLDecoder; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; @@ -66,12 +69,17 @@ public class FirebaseAuthIT { - private static final String ID_TOOLKIT_URL = + private static final String VERIFY_CUSTOM_TOKEN_URL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"; - private static final String ID_TOOLKIT_URL2 = + private static final String VERIFY_PASSWORD_URL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword"; + private static final String RESET_PASSWORD_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword"; + private static final String EMAIL_LINK_SIGN_IN_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin"; private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); private static final HttpTransport transport = Utils.getDefaultTransport(); + public static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3"; private static FirebaseAuth auth; @@ -129,24 +137,13 @@ public void testDeleteNonExistingUser() throws Exception { } } - private String randomPhoneNumber() { - Random random = new Random(); - StringBuilder builder = new StringBuilder("+1"); - for (int i = 0; i < 10; i++) { - builder.append(random.nextInt(10)); - } - return builder.toString(); - } - @Test public void testCreateUserWithParams() throws Exception { - String randomId = UUID.randomUUID().toString().replaceAll("-", ""); - String userEmail = ("test" + randomId.substring(0, 12) + "@example." + randomId.substring(12) - + ".com").toLowerCase(); + RandomUser randomUser = RandomUser.create(); String phone = randomPhoneNumber(); CreateRequest user = new CreateRequest() - .setUid(randomId) - .setEmail(userEmail) + .setUid(randomUser.uid) + .setEmail(randomUser.email) .setPhoneNumber(phone) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") @@ -155,9 +152,9 @@ public void testCreateUserWithParams() throws Exception { UserRecord userRecord = auth.createUserAsync(user).get(); try { - assertEquals(randomId, userRecord.getUid()); + assertEquals(randomUser.uid, userRecord.getUid()); assertEquals("Random User", userRecord.getDisplayName()); - assertEquals(userEmail, userRecord.getEmail()); + assertEquals(randomUser.email, userRecord.getEmail()); assertEquals(phone, userRecord.getPhoneNumber()); assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -171,7 +168,7 @@ public void testCreateUserWithParams() throws Exception { assertTrue(providers.contains("password")); assertTrue(providers.contains("phone")); - checkRecreate(randomId); + checkRecreate(randomUser.uid); } finally { auth.deleteUserAsync(userRecord.getUid()).get(); } @@ -198,13 +195,11 @@ public void testUserLifecycle() throws Exception { assertTrue(userRecord.getCustomClaims().isEmpty()); // Update user - String randomId = UUID.randomUUID().toString().replaceAll("-", ""); - String userEmail = ("test" + randomId.substring(0, 12) + "@example." + randomId.substring(12) - + ".com").toLowerCase(); + RandomUser randomUser = RandomUser.create(); String phone = randomPhoneNumber(); UpdateRequest request = userRecord.updateRequest() .setDisplayName("Updated Name") - .setEmail(userEmail) + .setEmail(randomUser.email) .setPhoneNumber(phone) .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) @@ -212,7 +207,7 @@ public void testUserLifecycle() throws Exception { userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); assertEquals("Updated Name", userRecord.getDisplayName()); - assertEquals(userEmail, userRecord.getEmail()); + assertEquals(randomUser.email, userRecord.getEmail()); assertEquals(phone, userRecord.getPhoneNumber()); assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -233,7 +228,7 @@ public void testUserLifecycle() throws Exception { userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); assertNull(userRecord.getDisplayName()); - assertEquals(userEmail, userRecord.getEmail()); + assertEquals(randomUser.email, userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); assertNull(userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -312,7 +307,7 @@ public void onSuccess(ListUsersPage result) { } semaphore.release(); } - }); + }, MoreExecutors.directExecutor()); semaphore.acquire(); assertEquals(uids.size(), collected.get()); assertNull(error.get()); @@ -378,7 +373,7 @@ public void testCustomTokenWithIAM() throws Exception { token = credentials.refreshAccessToken(); } FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(GoogleCredentials.of(token)) + .setCredentials(GoogleCredentials.create(token)) .setServiceAccountId(((ServiceAccountSigner) credentials).getAccount()) .setProjectId(IntegrationTestUtils.getProjectId()) .build(); @@ -470,12 +465,10 @@ public void testCustomTokenWithClaims() throws Exception { @Test public void testImportUsers() throws Exception { - final String randomId = UUID.randomUUID().toString().replaceAll("-", ""); - final String userEmail = ("test" + randomId.substring(0, 12) + "@example." - + randomId.substring(12) + ".com").toLowerCase(); + RandomUser randomUser = RandomUser.create(); ImportUserRecord user = ImportUserRecord.builder() - .setUid(randomId) - .setEmail(userEmail) + .setUid(randomUser.uid) + .setEmail(randomUser.email) .build(); UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get(); @@ -483,23 +476,21 @@ public void testImportUsers() throws Exception { assertEquals(0, result.getFailureCount()); try { - UserRecord savedUser = auth.getUserAsync(randomId).get(); - assertEquals(userEmail, savedUser.getEmail()); + UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); + assertEquals(randomUser.email, savedUser.getEmail()); } finally { - auth.deleteUserAsync(randomId).get(); + auth.deleteUserAsync(randomUser.uid).get(); } } @Test public void testImportUsersWithPassword() throws Exception { - final String randomId = UUID.randomUUID().toString().replaceAll("-", ""); - final String userEmail = ("test" + randomId.substring(0, 12) + "@example." - + randomId.substring(12) + ".com").toLowerCase(); + RandomUser randomUser = RandomUser.create(); final byte[] passwordHash = BaseEncoding.base64().decode( "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=="); ImportUserRecord user = ImportUserRecord.builder() - .setUid(randomId) - .setEmail(userEmail) + .setUid(randomUser.uid) + .setEmail(randomUser.email) .setPasswordHash(passwordHash) .setPasswordSalt("NaCl".getBytes()) .build(); @@ -519,17 +510,112 @@ public void testImportUsersWithPassword() throws Exception { assertEquals(0, result.getFailureCount()); try { - UserRecord savedUser = auth.getUserAsync(randomId).get(); - assertEquals(userEmail, savedUser.getEmail()); - String idToken = signInWithPassword(userEmail, "password"); + UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); + assertEquals(randomUser.email, savedUser.getEmail()); + String idToken = signInWithPassword(randomUser.email, "password"); assertFalse(Strings.isNullOrEmpty(idToken)); } finally { - auth.deleteUserAsync(randomId).get(); + auth.deleteUserAsync(randomUser.uid).get(); + } + } + + @Test + public void testGeneratePasswordResetLink() throws Exception { + RandomUser user = RandomUser.create(); + auth.createUser(new CreateRequest() + .setUid(user.uid) + .setEmail(user.email) + .setEmailVerified(false) + .setPassword("password")); + try { + String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder() + .setUrl(ACTION_LINK_CONTINUE_URL) + .setHandleCodeInApp(false) + .build()); + Map linkParams = parseLinkParameters(link); + assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); + String email = resetPassword(user.email, "password", "newpassword", + linkParams.get("oobCode")); + assertEquals(user.email, email); + // Password reset also verifies the user's email + assertTrue(auth.getUser(user.uid).isEmailVerified()); + } finally { + auth.deleteUser(user.uid); + } + } + + @Test + public void testGenerateEmailVerificationResetLink() throws Exception { + RandomUser user = RandomUser.create(); + auth.createUser(new CreateRequest() + .setUid(user.uid) + .setEmail(user.email) + .setEmailVerified(false) + .setPassword("password")); + try { + String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder() + .setUrl(ACTION_LINK_CONTINUE_URL) + .setHandleCodeInApp(false) + .build()); + Map linkParams = parseLinkParameters(link); + assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); + // There doesn't seem to be a public API for verifying an email, so we cannot do a more + // thorough test here. + assertEquals("verifyEmail", linkParams.get("mode")); + } finally { + auth.deleteUser(user.uid); + } + } + + @Test + public void testGenerateSignInWithEmailLink() throws Exception { + RandomUser user = RandomUser.create(); + auth.createUser(new CreateRequest() + .setUid(user.uid) + .setEmail(user.email) + .setEmailVerified(false) + .setPassword("password")); + try { + String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder() + .setUrl(ACTION_LINK_CONTINUE_URL) + .setHandleCodeInApp(false) + .build()); + Map linkParams = parseLinkParameters(link); + assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); + String idToken = signInWithEmailLink(user.email, linkParams.get("oobCode")); + assertFalse(Strings.isNullOrEmpty(idToken)); + assertTrue(auth.getUser(user.uid).isEmailVerified()); + } finally { + auth.deleteUser(user.uid); + } + } + + private Map parseLinkParameters(String link) throws Exception { + Map result = new HashMap<>(); + int queryBegin = link.indexOf('?'); + if (queryBegin != -1) { + String[] segments = link.substring(queryBegin + 1).split("&"); + for (String segment : segments) { + int equalSign = segment.indexOf('='); + String key = segment.substring(0, equalSign); + String value = segment.substring(equalSign + 1); + result.put(key, URLDecoder.decode(value, "UTF-8")); + } + } + return result; + } + + private String randomPhoneNumber() { + Random random = new Random(); + StringBuilder builder = new StringBuilder("+1"); + for (int i = 0; i < 10; i++) { + builder.append(random.nextInt(10)); } + return builder.toString(); } private String signInWithCustomToken(String customToken) throws IOException { - GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + "?key=" + GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + IntegrationTestUtils.getApiKey()); Map content = ImmutableMap.of( "token", customToken, "returnSecureToken", true); @@ -546,7 +632,7 @@ private String signInWithCustomToken(String customToken) throws IOException { } private String signInWithPassword(String email, String password) throws IOException { - GenericUrl url = new GenericUrl(ID_TOOLKIT_URL2 + "?key=" + GenericUrl url = new GenericUrl(VERIFY_PASSWORD_URL + "?key=" + IntegrationTestUtils.getApiKey()); Map content = ImmutableMap.of( "email", email, "password", password); @@ -562,6 +648,42 @@ private String signInWithPassword(String email, String password) throws IOExcept } } + private String resetPassword( + String email, String oldPassword, String newPassword, String oobCode) throws IOException { + GenericUrl url = new GenericUrl(RESET_PASSWORD_URL + "?key=" + + IntegrationTestUtils.getApiKey()); + Map content = ImmutableMap.of( + "email", email, "oldPassword", oldPassword, "newPassword", newPassword, "oobCode", oobCode); + HttpRequest request = transport.createRequestFactory().buildPostRequest(url, + new JsonHttpContent(jsonFactory, content)); + request.setParser(new JsonObjectParser(jsonFactory)); + HttpResponse response = request.execute(); + try { + GenericJson json = response.parseAs(GenericJson.class); + return json.get("email").toString(); + } finally { + response.disconnect(); + } + } + + private String signInWithEmailLink( + String email, String oobCode) throws IOException { + GenericUrl url = new GenericUrl(EMAIL_LINK_SIGN_IN_URL + "?key=" + + IntegrationTestUtils.getApiKey()); + Map content = ImmutableMap.of( + "email", email, "oobCode", oobCode); + HttpRequest request = transport.createRequestFactory().buildPostRequest(url, + new JsonHttpContent(jsonFactory, content)); + request.setParser(new JsonObjectParser(jsonFactory)); + HttpResponse response = request.execute(); + try { + GenericJson json = response.parseAs(GenericJson.class); + return json.get("idToken").toString(); + } finally { + response.disconnect(); + } + } + private void checkRecreate(String uid) throws Exception { try { auth.createUserAsync(new CreateRequest().setUid(uid)).get(); @@ -571,4 +693,21 @@ private void checkRecreate(String uid) throws Exception { assertEquals("uid-already-exists", ((FirebaseAuthException) e.getCause()).getErrorCode()); } } + + private static class RandomUser { + private final String uid; + private final String email; + + private RandomUser(String uid, String email) { + this.uid = uid; + this.email = email; + } + + static RandomUser create() { + final String uid = UUID.randomUUID().toString().replaceAll("-", ""); + final String email = ("test" + uid.substring(0, 12) + "@example." + + uid.substring(12) + ".com").toLowerCase(); + return new RandomUser(uid, email); + } + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index fe6f00071..72e81fd19 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -39,6 +39,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.internal.SdkUtils; @@ -62,6 +63,17 @@ public class FirebaseUserManagerTest { private static final String TEST_TOKEN = "token"; private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + private static final ActionCodeSettings ACTION_CODE_SETTINGS = ActionCodeSettings.builder() + .setUrl("https://example.dynamic.link") + .setHandleCodeInApp(true) + .setDynamicLinkDomain("custom.page.link") + .setIosBundleId("com.example.ios") + .setAndroidPackageName("com.example.android") + .setAndroidInstallApp(true) + .setAndroidMinimumVersion("6") + .build(); + private static final Map ACTION_CODE_SETTINGS_MAP = + ACTION_CODE_SETTINGS.getProperties(); @After public void tearDown() { @@ -988,6 +1000,213 @@ public void testLargeCustomClaims() { } } + @Test + public void testGeneratePasswordResetLinkNoEmail() throws Exception { + initializeAppForUserManagement(); + try { + FirebaseAuth.getInstance().generatePasswordResetLinkAsync(null).get(); + fail("No error thrown for null email"); + } catch (IllegalArgumentException expected) { + } + + try { + FirebaseAuth.getInstance().generatePasswordResetLinkAsync("").get(); + fail("No error thrown for empty email"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testGeneratePasswordResetLinkWithSettings() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + String link = FirebaseAuth.getInstance() + .generatePasswordResetLinkAsync("test@example.com", ACTION_CODE_SETTINGS).get(); + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("PASSWORD_RESET", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + for (Map.Entry entry : ACTION_CODE_SETTINGS_MAP.entrySet()) { + assertEquals(entry.getValue(), parsed.get(entry.getKey())); + } + } + + @Test + public void testGeneratePasswordResetLink() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + String link = FirebaseAuth.getInstance() + .generatePasswordResetLinkAsync("test@example.com").get(); + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + assertEquals(3, parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("PASSWORD_RESET", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + } + + @Test + public void testGenerateEmailVerificationLinkNoEmail() throws Exception { + initializeAppForUserManagement(); + try { + FirebaseAuth.getInstance().generateEmailVerificationLinkAsync(null).get(); + fail("No error thrown for null email"); + } catch (IllegalArgumentException expected) { + } + + try { + FirebaseAuth.getInstance().generateEmailVerificationLinkAsync("").get(); + fail("No error thrown for empty email"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testGenerateEmailVerificationLinkWithSettings() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + String link = FirebaseAuth.getInstance() + .generateEmailVerificationLinkAsync("test@example.com", ACTION_CODE_SETTINGS).get(); + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("VERIFY_EMAIL", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + for (Map.Entry entry : ACTION_CODE_SETTINGS_MAP.entrySet()) { + assertEquals(entry.getValue(), parsed.get(entry.getKey())); + } + } + + @Test + public void testGenerateEmailVerificationLink() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + String link = FirebaseAuth.getInstance() + .generateEmailVerificationLinkAsync("test@example.com").get(); + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + assertEquals(3, parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("VERIFY_EMAIL", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + } + + @Test + public void testGenerateESignInWithEmailLinkNoEmail() throws Exception { + initializeAppForUserManagement(); + try { + FirebaseAuth.getInstance().generateSignInWithEmailLinkAsync( + null, ACTION_CODE_SETTINGS).get(); + fail("No error thrown for null email"); + } catch (IllegalArgumentException expected) { + } + + try { + FirebaseAuth.getInstance().generateSignInWithEmailLinkAsync( + "", ACTION_CODE_SETTINGS).get(); + fail("No error thrown for empty email"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testGenerateESignInWithEmailLinkNullSettings() throws Exception { + initializeAppForUserManagement(); + try { + FirebaseAuth.getInstance().generateSignInWithEmailLinkAsync( + "test@example.com", null).get(); + fail("No error thrown for null email"); + } catch (NullPointerException expected) { + } + } + + @Test + public void testGenerateSignInWithEmailLinkWithSettings() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + String link = FirebaseAuth.getInstance() + .generateSignInWithEmailLinkAsync("test@example.com", ACTION_CODE_SETTINGS).get(); + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("EMAIL_SIGNIN", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + for (Map.Entry entry : ACTION_CODE_SETTINGS_MAP.entrySet()) { + assertEquals(entry.getValue(), parsed.get(entry.getKey())); + } + } + + @Test + public void testHttpErrorWithCode() { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of( + new MockLowLevelHttpResponse() + .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}") + .setStatusCode(500)))) + .setProjectId("test-project-id") + .build()); + FirebaseAuth auth = FirebaseAuth.getInstance(); + FirebaseUserManager userManager = auth.getUserManager(); + try { + userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); + fail("No exception thrown for HTTP error"); + } catch (FirebaseAuthException e) { + assertEquals("unauthorized-continue-uri", e.getErrorCode()); + assertTrue(e.getCause() instanceof HttpResponseException); + } + } + + @Test + public void testUnexpectedHttpError() { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of( + new MockLowLevelHttpResponse() + .setContent("{}") + .setStatusCode(500)))) + .setProjectId("test-project-id") + .build()); + FirebaseAuth auth = FirebaseAuth.getInstance(); + FirebaseUserManager userManager = auth.getUserManager(); + try { + userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); + fail("No exception thrown for HTTP error"); + } catch (FirebaseAuthException e) { + assertEquals("internal-error", e.getErrorCode()); + assertTrue(e.getCause() instanceof HttpResponseException); + } + } + private static TestResponseInterceptor initializeAppForUserManagement(String ...responses) { List mocks = new ArrayList<>(); for (String response : responses) { diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index 7d2b9d4ea..bbfc24ea6 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -113,6 +113,7 @@ public static synchronized FirebaseApp ensureDefaultApp() { .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .setFirestoreOptions(FirestoreOptions.newBuilder() .setTimestampsInSnapshotsEnabled(true) + .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .build()) .build(); masterApp = FirebaseApp.initializeApp(options); diff --git a/src/test/resources/generateEmailLink.json b/src/test/resources/generateEmailLink.json new file mode 100644 index 000000000..93f17083b --- /dev/null +++ b/src/test/resources/generateEmailLink.json @@ -0,0 +1,4 @@ +{ + "oobLink": "https://mock-oob-link.for.auth.tests", + "email": "test@example.com" +} From 5bf4d070dac32fffbe0000bd965a1490d861084b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 2 Jan 2019 11:50:25 -0800 Subject: [PATCH 030/441] Updated documentation and sample snippet (#235) --- src/main/java/com/google/firebase/FirebaseOptions.java | 3 ++- .../java/com/google/firebase/snippets/FirebaseAppSnippets.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index 99b4e1c5a..49c068e5d 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -296,7 +296,8 @@ public Builder setStorageBucket(String storageBucket) { } /** - * Sets the GoogleCredentials to use to authenticate the SDK. + * Sets the GoogleCredentials to use to authenticate the SDK. This parameter + * must be specified when creating a new instance of {@link FirebaseOptions}. * *

See * Initialize the SDK for code samples and detailed documentation. diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java index f4b76e752..ed1ad464e 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java @@ -121,9 +121,10 @@ public void initializeCustomApp() throws Exception { // [END access_services_nondefault] } - public void initializeWithServiceAccountId() { + public void initializeWithServiceAccountId() throws IOException { // [START initialize_sdk_with_service_account_id] FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.getApplicationDefault()) .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com") .build(); FirebaseApp.initializeApp(options); From 222f9c3ad9058707a072ea812ea04b84610c7908 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 7 Jan 2019 12:05:23 -0800 Subject: [PATCH 031/441] Sample snippets for the email action links API (#239) * Sample snippets for the email action links API * Updated snippets * Added comment --- .../snippets/FirebaseAuthSnippets.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index 153507dab..fec3f5d2d 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -17,6 +17,7 @@ package com.google.firebase.snippets; import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.ActionCodeSettings; import com.google.firebase.auth.ErrorInfo; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; @@ -596,4 +597,74 @@ public void importWithoutPassword() { } // [END import_without_password] } + + public ActionCodeSettings initActionCodeSettings() { + // [START init_action_code_settings] + ActionCodeSettings actionCodeSettings = ActionCodeSettings.builder() + .setUrl("https://www.example.com/checkout?cartId=1234") + .setHandleCodeInApp(true) + .setIosBundleId("com.example.ios") + .setAndroidPackageName("com.example.android") + .setAndroidInstallApp(true) + .setAndroidMinimumVersion("12") + .setDynamicLinkDomain("coolapp.page.link") + .build(); + // [END init_action_code_settings] + return actionCodeSettings; + } + + public void generatePasswordResetLink() { + final ActionCodeSettings actionCodeSettings = initActionCodeSettings(); + final String displayName = "Example User"; + // [START password_reset_link] + String email = "user@example.com"; + try { + String link = FirebaseAuth.getInstance().generatePasswordResetLink( + email, actionCodeSettings); + // Construct email verification template, embed the link and send + // using custom SMTP server. + sendCustomPasswordResetEmail(email, displayName, link); + } catch (FirebaseAuthException e) { + System.out.println("Error generating email link: " + e.getMessage()); + } + // [END password_reset_link] + } + + public void generateEmailVerificationLink() { + final ActionCodeSettings actionCodeSettings = initActionCodeSettings(); + final String displayName = "Example User"; + // [START email_verification_link] + String email = "user@example.com"; + try { + String link = FirebaseAuth.getInstance().generateEmailVerificationLink( + email, actionCodeSettings); + // Construct email verification template, embed the link and send + // using custom SMTP server. + sendCustomPasswordResetEmail(email, displayName, link); + } catch (FirebaseAuthException e) { + System.out.println("Error generating email link: " + e.getMessage()); + } + // [END email_verification_link] + } + + public void generateSignInWithEmailLink() { + final ActionCodeSettings actionCodeSettings = initActionCodeSettings(); + final String displayName = "Example User"; + // [START sign_in_with_email_link] + String email = "user@example.com"; + try { + String link = FirebaseAuth.getInstance().generateSignInWithEmailLink( + email, actionCodeSettings); + // Construct email verification template, embed the link and send + // using custom SMTP server. + sendCustomPasswordResetEmail(email, displayName, link); + } catch (FirebaseAuthException e) { + System.out.println("Error generating email link: " + e.getMessage()); + } + // [END sign_in_with_email_link] + } + + // Place holder method to make the compiler happy. This is referenced by all email action + // link snippets. + private void sendCustomPasswordResetEmail(String email, String displayName, String link) {} } From 4cbb0b90d1e613c88a15065e90a704ca14219458 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 8 Jan 2019 14:14:50 -0800 Subject: [PATCH 032/441] Some documentation updates (#240) --- .../firebase/auth/ActionCodeSettings.java | 4 +++- .../google/firebase/auth/FirebaseAuth.java | 20 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/ActionCodeSettings.java b/src/main/java/com/google/firebase/auth/ActionCodeSettings.java index 67a196edf..0b102b7a3 100644 --- a/src/main/java/com/google/firebase/auth/ActionCodeSettings.java +++ b/src/main/java/com/google/firebase/auth/ActionCodeSettings.java @@ -92,7 +92,9 @@ public static final class Builder { private Builder() { } /** - * Sets the link continue/state URL, which has different meanings in different contexts: + * Sets the link continue/state URL. + * + *

This parameter has different meanings in different contexts: * *